voxflow 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
(()=>{var e={6:(e,t,o)=>{const{getToken:n,clearToken:s,getTokenInfo:r}=o(986);const{story:a,ApiError:i}=o(214);const{podcast:c,ApiError:l}=o(35);const{synthesize:u}=o(383);const{narrate:p}=o(80);const{voices:d}=o(784);const{dub:f}=o(944);const{asr:g,ASR_DEFAULTS:m}=o(929);const{translate:h}=o(585);const{videoTranslate:w}=o(863);const{warnIfMissingFfmpeg:x}=o(297);const{API_BASE:v,WEB_BASE:S,STORY_DEFAULTS:$,PODCAST_DEFAULTS:y,SYNTHESIZE_DEFAULTS:b,NARRATE_DEFAULTS:T,DUB_DEFAULTS:k,ASR_DEFAULTS:F,TRANSLATE_DEFAULTS:E,VIDEO_TRANSLATE_DEFAULTS:_,getConfigDir:A}=o(782);const I=o(330);const M=i;async function run(e){const t=e||process.argv.slice(2);const o=t[0];if(!o||o==="--help"||o==="-h"){printHelp();return}if(o==="--version"||o==="-v"){console.log(I.version);return}switch(o){case"login":return handleLogin(t.slice(1));case"logout":return handleLogout();case"status":return handleStatus();case"story":case"generate":return handleStory(t.slice(1));case"podcast":return handlePodcast(t.slice(1));case"synthesize":case"say":return handleSynthesize(t.slice(1));case"narrate":return handleNarrate(t.slice(1));case"voices":return handleVoices(t.slice(1));case"dub":return handleDub(t.slice(1));case"asr":case"transcribe":return handleAsr(t.slice(1));case"translate":return handleTranslate(t.slice(1));case"video-translate":return handleVideoTranslate(t.slice(1));case"dashboard":return handleDashboard();default:console.error(`Unknown command: ${o}\nRun voxflow --help for usage.`);process.exit(1)}}async function handleLogin(e){const t=parseFlag(e,"--api")||v;console.log("Logging in...");const o=await n({api:t,force:true});const s=r();if(s){console.log(`\n[32mLogged in as ${s.email}[0m`);console.log(`Token expires: ${s.expiresAt}`);console.log(`API: ${s.api}`)}}function handleLogout(){s();console.log("Logged out. Token cache cleared.")}function handleStatus(){const e=r();if(!e){console.log("Not logged in. Run: voxflow login");return}console.log(`Email: ${e.email}`);console.log(`API: ${e.api}`);console.log(`Expires: ${e.expiresAt}`);console.log(`Valid: ${e.valid?"[32myes[0m":"[31mexpired[0m"}`);if(!e.valid){console.log("\nToken expired. Run: voxflow login")}console.log(`\nDashboard: ${S}/app/`);console.log("Run [36mvoxflow dashboard[0m to open in browser.")}async function handleStory(e){const t=parseFlag(e,"--api")||v;const o=parseFlag(e,"--token");let s;if(o){s=o}else{s=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseIntFlag(e,"--paragraphs");const c=parseFloatFlag(e,"--speed");const l=parseFloatFlag(e,"--silence");const u=parseFlag(e,"--output");if(i!==undefined){if(isNaN(i)||i<1||i>20){console.error(`Error: --paragraphs must be an integer between 1 and 20 (got: "${parseFlag(e,"--paragraphs")}")`);process.exit(1)}}validateSpeed(e,c);validateSilence(e,l);validateOutput(u);const p={token:s,api:t,topic:parseFlag(e,"--topic"),voice:parseFlag(e,"--voice"),output:u,paragraphs:i,speed:c,silence:l};await runWithRetry(a,p,t,o)}async function handlePodcast(e){const t=parseFlag(e,"--api")||v;const o=parseFlag(e,"--token");let s;if(o){s=o}else{s=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const a=parseIntFlag(e,"--exchanges");const i=parseFloatFlag(e,"--speed");const l=parseFloatFlag(e,"--silence");const u=parseFlag(e,"--output");if(a!==undefined){if(isNaN(a)||a<2||a>30){console.error(`Error: --exchanges must be an integer between 2 and 30 (got: "${parseFlag(e,"--exchanges")}")`);process.exit(1)}}validateSpeed(e,i);validateSilence(e,l);validateOutput(u);const p=parseFlag(e,"--length");if(p&&!["short","medium","long"].includes(p)){console.error(`Error: --length must be one of: short, medium, long (got: "${p}")`);process.exit(1)}const d={token:s,api:t,topic:parseFlag(e,"--topic"),style:parseFlag(e,"--style"),length:p,exchanges:a,output:u,speed:i,silence:l};await runWithRetry(c,d,t,o)}async function handleSynthesize(e){const t=parseFlag(e,"--api")||v;const o=parseFlag(e,"--token");let s;if(o){s=o}else{s=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}let a=parseFlag(e,"--text");if(!a){const t=new Set(["--text","--voice","--speed","--volume","--pitch","--output","--token","--api","--format"]);for(let o=0;o<e.length;o++){if(e[o].startsWith("--")){if(t.has(e[o]))o++;continue}a=e[o];break}}if(!a){console.error('Error: No text provided. Usage: voxflow synthesize "your text here"');process.exit(1)}const i=parseFloatFlag(e,"--speed");const c=parseFloatFlag(e,"--volume");const l=parseFloatFlag(e,"--pitch");const p=parseFlag(e,"--output");const d=parseFlag(e,"--format");validateSpeed(e,i);validateOutput(p,d);validateFormat(d);if(c!==undefined){if(isNaN(c)||c<.1||c>2){console.error(`Error: --volume must be between 0.1 and 2.0 (got: "${parseFlag(e,"--volume")}")`);process.exit(1)}}if(l!==undefined){if(isNaN(l)||l<-12||l>12){console.error(`Error: --pitch must be between -12 and 12 (got: "${parseFlag(e,"--pitch")}")`);process.exit(1)}}const f={token:s,api:t,text:a,voice:parseFlag(e,"--voice"),output:p,speed:i,volume:c,pitch:l,format:d||undefined};await runWithRetry(u,f,t,o)}async function handleNarrate(e){const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--input");const c=parseFlag(e,"--text");const l=parseFlag(e,"--script");const u=parseFloatFlag(e,"--speed");const d=parseFloatFlag(e,"--silence");const f=parseFlag(e,"--output");const g=parseFlag(e,"--format");validateSpeed(e,u);validateSilence(e,d);validateOutput(f,g);validateFormat(g);if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: Input file not found: ${n}`);process.exit(1)}}if(l){const e=o(896);const t=o(928);const n=t.resolve(l);if(!e.existsSync(n)){console.error(`Error: Script file not found: ${n}`);process.exit(1)}}const m={token:a,api:t,input:i,text:c,script:l,voice:parseFlag(e,"--voice"),output:f,speed:u,silence:d,format:g||undefined};await runWithRetry(p,m,t,s)}async function handleVoices(e){const t=parseFlag(e,"--api")||v;const o={api:t,search:parseFlag(e,"--search"),gender:parseFlag(e,"--gender"),language:parseFlag(e,"--language"),json:parseBoolFlag(e,"--json"),extended:parseBoolFlag(e,"--extended")};await d(o)}async function handleDub(e){await x(A(),"dub");const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--srt");const c=parseFlag(e,"--video");const l=parseFlag(e,"--output");const u=parseFloatFlag(e,"--speed");const p=parseFloatFlag(e,"--ducking");const d=parseIntFlag(e,"--patch");if(!i&&!parseBoolFlag(e,"--help")){console.error("Error: --srt <file> is required. Usage: voxflow dub --srt <file.srt>");process.exit(1)}if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: SRT file not found: ${n}`);process.exit(1)}}if(c){const e=o(896);const t=o(928);const n=t.resolve(c);if(!e.existsSync(n)){console.error(`Error: Video file not found: ${n}`);process.exit(1)}}const g=parseFlag(e,"--voices");if(g){const e=o(896);const t=o(928);const n=t.resolve(g);if(!e.existsSync(n)){console.error(`Error: Voices map file not found: ${n}`);process.exit(1)}}const m=parseFlag(e,"--bgm");if(m){const e=o(896);const t=o(928);const n=t.resolve(m);if(!e.existsSync(n)){console.error(`Error: BGM file not found: ${n}`);process.exit(1)}}validateSpeed(e,u);if(l){const e=c?[".mp4",".mkv",".mov"]:[".wav",".mp3"];const t=e.some((e=>l.toLowerCase().endsWith(e)));if(!t){const t=e.join(", ");console.error(`Error: --output path must end with ${t}`);process.exit(1)}}if(p!==undefined){if(isNaN(p)||p<0||p>1){console.error(`Error: --ducking must be between 0 and 1.0 (got: "${parseFlag(e,"--ducking")}")`);process.exit(1)}}const h={token:a,api:t,srt:i,video:c,output:l,speed:u,patch:d,voice:parseFlag(e,"--voice"),voicesMap:g,speedAuto:parseBoolFlag(e,"--speed-auto"),bgm:m,ducking:p};await runWithRetry(f,h,t,s)}async function handleAsr(e){await x(A(),"asr");const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");const a=parseFlag(e,"--engine")||F.engine;const i=parseFlag(e,"--model")||F.model;if(a&&!["auto","local","cloud","whisper","tencent"].includes(a)){console.error(`Error: --engine must be one of: auto, local, cloud (got: "${a}")`);process.exit(1)}if(i&&!["tiny","base","small","medium","large"].includes(i)){console.error(`Error: --model must be one of: tiny, base, small, medium, large (got: "${i}")`);process.exit(1)}const c=a==="local"||a==="whisper";let l;if(c){l=null}else if(s){l=s}else{l=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const u=parseFlag(e,"--input");const p=parseFlag(e,"--url");const d=parseBoolFlag(e,"--mic");const f=parseFlag(e,"--mode")||m.mode;const h=parseFlag(e,"--lang")||parseFlag(e,"--language")||m.lang;const w=parseFlag(e,"--format")||m.format;const S=parseFlag(e,"--output");const $=parseBoolFlag(e,"--speakers");const y=parseIntFlag(e,"--speaker-number");const b=parseIntFlag(e,"--task-id");if(f&&!["auto","sentence","flash","file"].includes(f)){console.error(`Error: --mode must be one of: auto, sentence, flash, file (got: "${f}")`);process.exit(1)}if(w&&!["srt","txt","json"].includes(w)){console.error(`Error: --format must be one of: srt, txt, json (got: "${w}")`);process.exit(1)}if(u){const e=o(896);const t=o(928);const n=t.resolve(u);if(!e.existsSync(n)){console.error(`Error: Input file not found: ${n}`);process.exit(1)}}const T={token:l,api:t,input:u,url:p,mic:d,mode:f,lang:h,format:w,output:S,speakers:$,speakerNumber:y,taskId:b,engine:a,model:i};if(c){await g(T)}else{await runWithRetry(g,T,t,s)}}async function handleTranslate(e){const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--srt");const c=parseFlag(e,"--text");const l=parseFlag(e,"--input");const u=parseFlag(e,"--from");const p=parseFlag(e,"--to");const d=parseFlag(e,"--output");const f=parseBoolFlag(e,"--realign");const g=parseIntFlag(e,"--batch-size");if(!p&&!parseBoolFlag(e,"--help")){console.error("Error: --to <lang> is required. Example: voxflow translate --srt file.srt --to en");process.exit(1)}const m=["zh","en","ja","ko","fr","de","es","pt","ru","ar","th","vi","it"];if(p&&!m.includes(p)){console.error(`Error: --to must be one of: ${m.join(", ")} (got: "${p}")`);process.exit(1)}if(u&&!m.includes(u)&&u!=="auto"){console.error(`Error: --from must be one of: auto, ${m.join(", ")} (got: "${u}")`);process.exit(1)}const w=[i,c,l].filter(Boolean).length;if(w===0&&!parseBoolFlag(e,"--help")){console.error("Error: Provide one of: --srt <file>, --text <text>, --input <file>");process.exit(1)}if(w>1){console.error("Error: Specify only one input: --srt, --text, or --input");process.exit(1)}if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: SRT file not found: ${n}`);process.exit(1)}}if(l){const e=o(896);const t=o(928);const n=t.resolve(l);if(!e.existsSync(n)){console.error(`Error: Input file not found: ${n}`);process.exit(1)}}if(g!==undefined){if(isNaN(g)||g<1||g>20){console.error(`Error: --batch-size must be between 1 and 20 (got: "${parseFlag(e,"--batch-size")}")`);process.exit(1)}}const x={token:a,api:t,srt:i,text:c,input:l,from:u,to:p,output:d,realign:f,batchSize:g};await runWithRetry(h,x,t,s)}async function handleVideoTranslate(e){if(parseBoolFlag(e,"--help")||parseBoolFlag(e,"-h")){printHelp();return}const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--input");const c=parseFlag(e,"--from");const l=parseFlag(e,"--to");const u=parseFlag(e,"--voice");const p=parseFlag(e,"--voices");const d=parseFlag(e,"--output");const f=parseBoolFlag(e,"--realign");const g=parseBoolFlag(e,"--keep-intermediates");const m=parseIntFlag(e,"--batch-size");const h=parseFloatFlag(e,"--speed");const x=parseFlag(e,"--asr-mode");const S=parseFlag(e,"--asr-lang");if(!i){console.error("Error: --input <video-file> is required. Example: voxflow video-translate --input video.mp4 --to en");process.exit(1)}if(!l){console.error("Error: --to <lang> is required. Example: voxflow video-translate --input video.mp4 --to en");process.exit(1)}const $=["zh","en","ja","ko","fr","de","es","pt","ru","ar","th","vi","it"];if(l&&!$.includes(l)){console.error(`Error: --to must be one of: ${$.join(", ")} (got: "${l}")`);process.exit(1)}if(c&&!$.includes(c)&&c!=="auto"){console.error(`Error: --from must be one of: auto, ${$.join(", ")} (got: "${c}")`);process.exit(1)}if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: Video file not found: ${n}`);process.exit(1)}}if(h!==undefined&&(isNaN(h)||h<.5||h>2)){console.error(`Error: --speed must be between 0.5 and 2.0 (got: "${parseFlag(e,"--speed")}")`);process.exit(1)}if(m!==undefined&&(isNaN(m)||m<1||m>20)){console.error(`Error: --batch-size must be between 1 and 20 (got: "${parseFlag(e,"--batch-size")}")`);process.exit(1)}const y=["auto","sentence","flash","file"];if(x&&!y.includes(x)){console.error(`Error: --asr-mode must be one of: ${y.join(", ")} (got: "${x}")`);process.exit(1)}if(p){const e=o(896);const t=o(928);const n=t.resolve(p);if(!e.existsSync(n)){console.error(`Error: Voices map file not found: ${n}`);process.exit(1)}}const b={token:a,api:t,input:i,from:c,to:l,voice:u,voicesMap:p,output:d,realign:f,keepIntermediates:g,batchSize:m,speed:h,asrMode:x,asrLang:S};await runWithRetry(w,b,t,s)}async function handleDashboard(){const e=`${S}/app/`;console.log(`\nOpening dashboard: ${e}`);try{const t=(await o.e(935).then(o.bind(o,935))).default;const n=await t(e);if(n&&typeof n.on==="function"){n.on("error",(()=>{console.log("[31mFailed to open browser. Visit manually:[0m");console.log(` ${e}`)}))}}catch{console.log("[31mFailed to open browser. Visit manually:[0m");console.log(` ${e}`)}}async function runWithRetry(e,t,o,s){try{await e(t)}catch(r){if(r instanceof M&&r.code==="token_expired"&&!s){console.log("\nToken expired, re-authenticating...");t.token=await n({api:o,force:true});await e(t)}else{throw r}}}function validateSpeed(e,t){if(t!==undefined){if(isNaN(t)||t<.5||t>2){console.error(`Error: --speed must be between 0.5 and 2.0 (got: "${parseFlag(e,"--speed")}")`);process.exit(1)}}}function validateSilence(e,t){if(t!==undefined){if(isNaN(t)||t<0||t>5){console.error(`Error: --silence must be between 0 and 5.0 (got: "${parseFlag(e,"--silence")}")`);process.exit(1)}}}function validateOutput(e,t){if(e){const t=[".wav",".mp3"];const o=t.some((t=>e.toLowerCase().endsWith(t)));if(!o){console.error("Error: --output path must end with .wav or .mp3");process.exit(1)}}}function validateFormat(e){if(e&&!["pcm","wav","mp3"].includes(e)){console.error(`Error: --format must be one of: pcm, wav, mp3 (got: "${e}")`);process.exit(1)}}function parseFlag(e,t){const o=e.indexOf(t);if(o===-1||o+1>=e.length)return null;return e[o+1]}function parseIntFlag(e,t){const o=parseFlag(e,t);return o!=null?parseInt(o,10):undefined}function parseFloatFlag(e,t){const o=parseFlag(e,t);return o!=null?parseFloat(o):undefined}function parseBoolFlag(e,t){return e.includes(t)}function printHelp(){console.log(`\nvoxflow v${I.version} — AI audio content creation CLI\n\nUsage:\n voxflow <command> [options]\n\nCommands:\n login Open browser to login and cache token\n logout Clear cached token\n status Show login status and token info\n dashboard Open Web dashboard in browser\n story [opts] Generate a story with TTS narration\n podcast [opts] Generate a multi-speaker podcast/dialogue\n synthesize <text> Synthesize a single text snippet to audio (alias: say)\n narrate [opts] Narrate a file, text, or script to audio\n voices [opts] Browse and search available TTS voices\n dub [opts] Dub video/audio from SRT subtitles (timeline-aligned TTS)\n asr [opts] Transcribe audio/video to text (alias: transcribe)\n translate [opts] Translate SRT subtitles, text, or files\n video-translate Translate entire video: ASR → translate → dub → merge\n\nStory options:\n --topic <text> Story topic (default: children's story)\n --voice <id> TTS voice ID (default: ${$.voice})\n --output <path> Output WAV path (default: ./story-<timestamp>.wav)\n --paragraphs <n> Paragraph count, 1-20 (default: ${$.paragraphs})\n --speed <n> TTS speed 0.5-2.0 (default: ${$.speed})\n --silence <sec> Silence between paragraphs, 0-5.0 (default: ${$.silence})\n\nPodcast options:\n --topic <text> Podcast topic (default: tech trends)\n --style <style> Dialogue style (default: ${y.style})\n --length <len> short | medium | long (default: ${y.length})\n --exchanges <n> Number of exchanges, 2-30 (default: ${y.exchanges})\n --output <path> Output WAV path (default: ./podcast-<timestamp>.wav)\n --speed <n> TTS speed 0.5-2.0 (default: ${y.speed})\n --silence <sec> Silence between segments, 0-5.0 (default: ${y.silence})\n\nSynthesize options (alias: say):\n <text> Text to synthesize (positional arg or --text)\n --text <text> Text to synthesize (alternative to positional)\n --voice <id> TTS voice ID (default: ${b.voice})\n --format <fmt> Output format: pcm, wav, mp3 (default: pcm → WAV)\n --speed <n> TTS speed 0.5-2.0 (default: ${b.speed})\n --volume <n> TTS volume 0.1-2.0 (default: ${b.volume})\n --pitch <n> TTS pitch -12 to 12 (default: ${b.pitch})\n --output <path> Output file path (default: ./tts-<timestamp>.wav)\n\nNarrate options:\n --input <file> Input .txt or .md file\n --text <text> Inline text to narrate\n --script <file> JSON script with per-segment voice/speed control\n --voice <id> Default voice ID (default: ${T.voice})\n --format <fmt> Output format: pcm, wav, mp3 (default: pcm → WAV)\n --speed <n> TTS speed 0.5-2.0 (default: ${T.speed})\n --silence <sec> Silence between segments, 0-5.0 (default: ${T.silence})\n --output <path> Output file path (default: ./narration-<timestamp>.wav)\n\n Also supports stdin: echo "text" | voxflow narrate\n\nDub options:\n --srt <file> SRT subtitle file (required)\n --video <file> Video file — merge dubbed audio into video\n --voice <id> Default TTS voice ID (default: ${k.voice})\n --voices <file> JSON speaker→voiceId map for multi-speaker dubbing\n --speed <n> TTS speed 0.5-2.0 (default: ${k.speed})\n --speed-auto Auto-adjust speed when audio overflows time slot\n --bgm <file> Background music file to mix in\n --ducking <n> BGM volume ducking 0-1.0 (default: ${k.ducking})\n --patch <id> Re-synthesize a single caption by ID (patch mode)\n --output <path> Output file path (default: ./dub-<timestamp>.wav)\n\nASR options (alias: transcribe):\n --input <file> Local audio or video file to transcribe\n --url <url> Remote audio URL to transcribe (cloud only)\n --mic Record from microphone (cloud only, requires sox)\n --engine <type> auto (default) | local | cloud\n --model <name> Whisper model: tiny, base (default), small, medium, large\n --mode <type> auto (default) | sentence | flash | file (cloud only)\n --lang <model> Language: 16k_zh (default), 16k_en, 16k_zh_en, 16k_ja, 16k_ko\n --format <fmt> Output format: srt (default), txt, json\n --output <path> Output file path (default: <input>.<format>)\n --speakers Enable speaker diarization (cloud flash/file mode)\n --speaker-number <n> Expected number of speakers (with --speakers)\n --task-id <id> Resume polling an existing async task (cloud only)\n\nTranslate options:\n --srt <file> SRT subtitle file to translate\n --text <text> Inline text to translate\n --input <file> Text file (.txt, .md) to translate\n --from <lang> Source language code (default: auto-detect)\n --to <lang> Target language code (required)\n --output <path> Output file path (default: <input>-<lang>.<ext>)\n --realign Adjust subtitle timing for target language length\n --batch-size <n> Captions per LLM call, 1-20 (default: ${E.batchSize})\n\n Supported languages: zh, en, ja, ko, fr, de, es, pt, ru, ar, th, vi, it\n\nVideo-translate options:\n --input <file> Input video file (required)\n --to <lang> Target language code (required)\n --from <lang> Source language code (default: auto-detect)\n --voice <id> TTS voice ID for dubbed audio\n --voices <file> JSON speaker→voiceId map for multi-speaker dubbing\n --realign Adjust subtitle timing for target language length\n --speed <n> TTS speed 0.5-2.0 (default: ${_.speed})\n --batch-size <n> Translation batch size, 1-20 (default: ${_.batchSize})\n --keep-intermediates Keep intermediate files (SRT, audio) for debugging\n --output <path> Output MP4 path (default: <input>-<lang>.mp4)\n --asr-mode <mode> Override ASR mode: auto, sentence, flash, file\n --asr-lang <engine> Override ASR engine: 16k_zh, 16k_en, 16k_ja, 16k_ko, etc.\n\nVoices options:\n --search <query> Search by name, tone, style, description\n --gender <m|f> Filter by gender: male/m or female/f\n --language <code> Filter by language: zh, en, etc.\n --extended Include extended voice library (380+ voices)\n --json Output raw JSON instead of table\n\nCommon options:\n --help, -h Show this help\n --version, -v Show version\n\nAdvanced options:\n --api <url> Override API endpoint (for self-hosted servers)\n --token <jwt> Use explicit token (CI/CD, skip browser login)\n\nExamples:\n voxflow say "你好世界"\n voxflow say "你好世界" --format mp3\n voxflow synthesize "Welcome" --voice v-male-Bk7vD3xP --format mp3\n voxflow narrate --input article.txt --voice v-female-R2s4N9qJ\n voxflow narrate --input article.txt --format mp3\n voxflow narrate --script narration-script.json\n echo "Hello" | voxflow narrate --output hello.wav\n voxflow voices --search "温柔" --gender female\n voxflow voices --extended --json\n voxflow dub --srt subtitles.srt\n voxflow dub --srt subtitles.srt --video input.mp4 --output dubbed.mp4\n voxflow dub --srt subtitles.srt --voices speakers.json --speed-auto\n voxflow dub --srt subtitles.srt --bgm music.mp3 --ducking 0.3\n voxflow dub --srt subtitles.srt --patch 5 --output dub-existing.wav\n voxflow asr --input recording.mp3\n voxflow asr --input recording.mp3 --engine local\n voxflow asr --input meeting.wav --engine local --model small\n voxflow asr --input video.mp4 --format srt --lang 16k_zh\n voxflow asr --url https://example.com/audio.wav --mode flash\n voxflow asr --mic --format txt\n voxflow transcribe --input meeting.wav --speakers --speaker-number 3\n voxflow asr --task-id 12345678 --format srt\n voxflow translate --srt subtitles.srt --to en\n voxflow translate --srt subtitles.srt --from zh --to en --realign\n voxflow translate --srt subtitles.srt --to ja --output subtitles-ja.srt\n voxflow translate --text "你好世界" --to en\n voxflow translate --input article.txt --to en --output article-en.txt\n voxflow video-translate --input video.mp4 --to en\n voxflow video-translate --input video.mp4 --from zh --to en --realign\n voxflow video-translate --input video.mp4 --to ja --voice v-male-Bk7vD3xP\n`)}e.exports={run:run}},929:(e,t,o)=>{const n=o(896);const s=o(928);const{API_BASE:r}=o(782);const{ApiError:a}=o(852);const{getMediaInfo:i,extractAudioForAsr:c}=o(388);const{uploadFileToCos:l}=o(567);const{recognize:u,detectMode:p,SENTENCE_MAX_MS:d,FLASH_MAX_MS:f,BASE64_MAX_BYTES:g,TASK_STATUS:m}=o(514);const{formatSrt:h,formatPlainText:w,formatJson:x,buildCaptionsFromFlash:v,buildCaptionsFromSentence:S,buildCaptionsFromFile:$}=o(813);const{checkWhisperAvailable:y,transcribeLocal:b}=o(126);const T={lang:"16k_zh",mode:"auto",format:"srt"};const k={"16k_zh":"中文 (16kHz)","16k_en":"English (16kHz)","16k_zh_en":"中英混合 (16kHz)","16k_ja":"日本語 (16kHz)","16k_ko":"한국어 (16kHz)","16k_zh_dialect":"中文方言 (16kHz)","8k_zh":"中文 (8kHz 电话)","8k_en":"English (8kHz phone)"};const F={srt:".srt",txt:".txt",json:".json"};async function asr(e){const sigintHandler=()=>{console.log("\n\nASR cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _asr(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _asr(e){const{token:t,api:a=r,input:d,url:f,mic:y=false,mode:b=T.mode,lang:E=T.lang,format:_=T.format,output:A,speakers:I=false,speakerNumber:M=0,taskId:L,engine:C="auto",model:N="base"}=e;if(L){return await resumePoll({apiBase:a,token:t,taskId:L,format:_,output:A,lang:E})}const P=resolveEngine(C);if(P==="local"){return await _asrLocal({input:d,format:_,output:A,model:N,lang:E})}const O=[d,f,y].filter(Boolean).length;if(O===0){throw new Error("No input specified. Provide one of:\n"+" --input <file> Local audio/video file\n"+" --url <url> Remote audio URL\n"+" --mic Record from microphone")}if(O>1){throw new Error("Specify only one input source: --input, --url, or --mic")}console.log("\n=== VoxFlow ASR ===");let D=d?s.resolve(d):null;let q=[];if(y){D=await handleMicInput();q.push(D)}if(D&&!n.existsSync(D)){throw new Error(`Input file not found: ${D}`)}let z=0;let R=0;let B=f||null;if(D){console.log(`Input: ${s.basename(D)}`);const e=await i(D);z=e.durationMs;R=n.statSync(D).size;const t=formatDuration(z);const o=formatSize(R);console.log(`Duration: ${t}`);console.log(`Size: ${o}`);if(!e.hasAudio){throw new Error("Input file has no audio track.")}console.log(`\n[1/3] 提取音频 (16kHz mono WAV)...`);const r=await c(D);q.push(r.wavPath);z=r.durationMs;R=n.statSync(r.wavPath).size;D=r.wavPath;console.log(` OK (${formatSize(R)}, ${formatDuration(z)})`)}else{console.log(`Input: ${f}`);console.log(`(Remote URL — duration will be detected by ASR API)`)}const U=!!B;const j=b==="auto"?p(z,U||!!D,R):b;console.log(`Mode: ${j}`);console.log(`Language: ${k[E]||E}`);console.log(`Format: ${_}`);if(D&&!B){const e=j==="flash"||j==="file"&&R>g||j==="sentence"&&R>g;if(e){console.log(`\n[2/3] 上传至 COS...`);const e=await l(D,a,t);B=e.cosUrl;console.log(` OK (${e.key})`)}else{console.log(`\n[2/3] 上传至 COS... (跳过,使用 base64)`)}}else if(!D&&B){console.log(`\n[2/3] 上传至 COS... (跳过,使用远程 URL)`)}console.log(`\n[3/3] ASR 语音识别 (${j})...`);const W=Date.now();const V=await u({apiBase:a,token:t,mode:j,url:B,filePath:j==="sentence"&&!B?D:undefined,durationMs:z,fileSize:R,lang:E,speakerDiarization:I,speakerNumber:M,wordInfo:_==="srt",onProgress:(e,t)=>{const o=e===m.WAITING?"排队中":e===m.PROCESSING?"识别中":"未知";process.stdout.write(`\r ${o}... (${Math.round(t/1e3)}s)`)}});const G=((Date.now()-W)/1e3).toFixed(1);console.log(`\n OK (${G}s)`);const K=V.audioTime||z/1e3||0;let J;switch(V.mode){case"flash":J=v(V.flashResult||[]);break;case"sentence":J=S(V.result,K,V.wordList);break;case"file":J=$(V.result,K);break;default:J=[{id:1,startMs:0,endMs:0,text:V.result||""}]}let H;switch(_){case"srt":H=h(J);break;case"txt":H=w(J,{includeSpeakers:I});break;case"json":H=x(J);break;default:throw new Error(`Unknown format: ${_}. Use: srt, txt, json`)}const Y=F[_]||".txt";let Q;if(A){Q=s.resolve(A)}else if(d){const e=s.basename(d,s.extname(d));Q=o.ab+"cli/"+s.dirname(d)+"/"+e+""+Y}else if(y){Q=s.resolve(`mic-${Date.now()}${Y}`)}else{try{const e=new URL(f);const t=s.basename(e.pathname,s.extname(e.pathname))||"asr";Q=o.ab+"cli/"+t+""+Y}catch{Q=s.resolve(`asr-${Date.now()}${Y}`)}}n.writeFileSync(Q,H,"utf8");for(const e of q){try{n.unlinkSync(e)}catch{}}const X=V.quota||{};const Z=1;const ee=X.remaining??"?";console.log(`\n=== Done ===`);console.log(`Output: ${Q}`);console.log(`Captions: ${J.length}`);console.log(`Duration: ${formatDuration(z||(V.audioTime||0)*1e3)}`);console.log(`Mode: ${V.mode}`);console.log(`Quota: ${Z} used, ${ee} remaining`);if(J.length>0&&_!=="json"){console.log(`\n--- Preview ---`);const e=J.slice(0,3);for(const t of e){const e=formatDuration(t.startMs);const o=t.speakerId?`[${t.speakerId}] `:"";const n=t.text.length>60?t.text.slice(0,57)+"...":t.text;console.log(` ${e} ${o}${n}`)}if(J.length>3){console.log(` ... (${J.length-3} more)`)}}return{outputPath:Q,mode:V.mode,duration:z/1e3,captionCount:J.length,quotaUsed:Z}}async function _asrLocal(e){const{input:t,format:r=T.format,output:a,model:l="base",lang:u=T.lang}=e;console.log("\n=== VoxFlow ASR (Local Whisper) ===");if(!t){throw new Error("Local whisper engine requires --input <file>.\n"+"URL and microphone input are cloud-only features.\n"+"Use: voxflow asr --input <file> --engine local")}const p=s.resolve(t);if(!n.existsSync(p)){throw new Error(`Input file not found: ${p}`)}const d=await i(p);console.log(`Input: ${s.basename(p)}`);console.log(`Duration: ${formatDuration(d.durationMs)}`);console.log(`Engine: whisper (local)`);console.log(`Model: ${l}`);console.log(`\n[1/2] 提取音频 (16kHz mono WAV)...`);const f=await c(p);console.log(` OK (${formatSize(n.statSync(f.wavPath).size)})`);console.log(`[2/2] Whisper 本地识别...`);const g=Date.now();const m=await b(f.wavPath,{model:l,lang:u});const v=((Date.now()-g)/1e3).toFixed(1);console.log(` OK (${v}s, ${m.length} segments)`);try{n.unlinkSync(f.wavPath)}catch{}let S;switch(r){case"srt":S=h(m);break;case"txt":S=w(m);break;case"json":S=x(m);break;default:throw new Error(`Unknown format: ${r}. Use: srt, txt, json`)}const $=F[r]||".txt";const y=a?s.resolve(a):o.ab+"cli/"+s.dirname(t)+"/"+s.basename(t,s.extname(t))+""+$;n.writeFileSync(y,S,"utf8");console.log(`\n=== Done ===`);console.log(`Output: ${y}`);console.log(`Captions: ${m.length}`);console.log(`Duration: ${formatDuration(d.durationMs)}`);console.log(`Engine: whisper (local, no quota used)`);if(m.length>0&&r!=="json"){console.log(`\n--- Preview ---`);const e=m.slice(0,3);for(const t of e){const e=formatDuration(t.startMs);const o=t.text.length>60?t.text.slice(0,57)+"...":t.text;console.log(` ${e} ${o}`)}if(m.length>3){console.log(` ... (${m.length-3} more)`)}}return{outputPath:y,mode:"local",duration:d.durationMs/1e3,captionCount:m.length,quotaUsed:0}}function resolveEngine(e){if(e==="local"||e==="whisper"){const e=y();if(!e.available){throw new Error("Local whisper engine requires nodejs-whisper.\n"+"Install: npm install -g nodejs-whisper\n"+"Download a model: npx nodejs-whisper download\n"+"Or use: --engine cloud")}return"local"}if(e==="cloud"||e==="tencent")return"cloud";if(e==="auto"){const{available:e}=y();return e?"local":"cloud"}return"cloud"}async function resumePoll({apiBase:e,token:t,taskId:r,format:a,output:i,lang:c}){console.log(`\n=== VoxFlow ASR — Resume Task ===`);console.log(`Task ID: ${r}`);const{pollTaskResult:l,TASK_STATUS:u}=o(514);console.log(`Polling...`);const p=await l({apiBase:e,token:t,taskId:r,onProgress:(e,t)=>{const o=e===u.WAITING?"排队中":e===u.PROCESSING?"识别中":"?";process.stdout.write(`\r ${o}... (${Math.round(t/1e3)}s)`)}});console.log(`\n OK`);const d=$(p.result,p.audioTime);let f;switch(a){case"srt":f=h(d);break;case"txt":f=w(d);break;case"json":f=x(d);break;default:f=w(d)}const g=F[a]||".txt";const m=i?s.resolve(i):s.resolve(`task-${r}${g}`);n.writeFileSync(m,f,"utf8");console.log(`\n=== Done ===`);console.log(`Output: ${m}`);console.log(`Captions: ${d.length}`);console.log(`Duration: ${formatDuration((p.audioTime||0)*1e3)}`);return{outputPath:m,mode:"file",duration:p.audioTime,captionCount:d.length}}async function handleMicInput(){const{recordMic:e,checkRecAvailable:t}=o(384);const n=await t();if(!n.available){throw new Error(n.error)}console.log(`\nRecording from microphone...`);console.log(` Press Enter or Q to stop recording.`);console.log(` Max duration: 5 minutes.\n`);const{wavPath:s,durationMs:r,stopped:a}=await e({maxSeconds:300});console.log(`\n Recording ${a==="user"?"stopped":"finished"}: ${formatDuration(r)}`);return s}function formatDuration(e){if(!e||e<=0)return"0s";const t=Math.round(e/1e3);if(t<60)return`${t}s`;const o=Math.floor(t/60);const n=t%60;if(o<60)return`${o}m${n>0?n+"s":""}`;const s=Math.floor(o/60);const r=o%60;return`${s}h${r>0?r+"m":""}`}function formatSize(e){if(e<1024)return`${e} B`;if(e<1024*1024)return`${(e/1024).toFixed(1)} KB`;return`${(e/1024/1024).toFixed(1)} MB`}e.exports={asr:asr,ASR_DEFAULTS:T,ApiError:a}},944:(e,t,o)=>{const n=o(896);const s=o(928);const{DUB_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{buildWav:u,getFileExtension:p}=o(56);const{parseSrt:d,formatSrt:f}=o(813);const{buildTimelinePcm:g,buildTimelineAudio:m,msToBytes:h,BYTES_PER_MS:w}=o(907);const{startSpinner:x}=o(339);function parseVoicesMap(e){if(!n.existsSync(e)){throw new Error(`Voices map file not found: ${e}`)}let t;try{t=JSON.parse(n.readFileSync(e,"utf8"))}catch(e){throw new Error(`Invalid JSON in voices map: ${e.message}`)}if(typeof t!=="object"||t===null||Array.isArray(t)){throw new Error('Voices map must be a JSON object: { "SpeakerName": "voiceId", ... }')}for(const[e,o]of Object.entries(t)){if(typeof o!=="string"||o.trim().length===0){throw new Error(`Invalid voice ID for speaker "${e}": must be a non-empty string`)}}return t}async function synthesizeCaption(e,t,o,n,s,r,l,u){process.stdout.write(` TTS [${l+1}/${u}]...`);const p={text:o,voiceId:n,speed:s??1,format:r||"pcm"};let d,f;try{({status:d,data:f}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},p))}catch(t){console.log(" FAIL");c(t,e)}if(d!==200||f.code!=="success"){console.log(" FAIL");i(d,f,`TTS caption ${l+1}`)}const g=Buffer.from(f.audio,"base64");const m=g.length/w;console.log(` OK (${(g.length/1024).toFixed(0)} KB, ${(m/1e3).toFixed(1)}s)`);return{audio:g,quota:f.quota,durationMs:m}}async function dub(e){const sigintHandler=()=>{console.log("\n\nDubbing cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _dub(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _dub(e){const t=e.srt;if(!t){throw new Error("No SRT file provided. Usage: voxflow dub --srt <file.srt>")}const a=s.resolve(t);if(!n.existsSync(a)){throw new Error(`SRT file not found: ${a}`)}const i=n.readFileSync(a,"utf8");const c=d(i);if(c.length===0){throw new Error("SRT file contains no valid captions")}const l=e.voice||r.voice;const u=e.speed??r.speed;const p=e.speedAuto||false;const f=r.toleranceMs;const g=e.api;const h=e.token;const w=e.patch;const x=[];let v=null;if(e.voicesMap){v=parseVoicesMap(s.resolve(e.voicesMap))}let S=e.output;const $=!!e.video;const y=$?".mp4":".wav";if(!S){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);S=s.resolve(`dub-${e}${y}`)}console.log("\n=== VoxFlow Dub ===");console.log(`SRT: ${t} (${c.length} captions)`);console.log(`Voice: ${l}${v?` + voices map (${Object.keys(v).length} speakers)`:""}`);console.log(`Speed: ${u}${p?" (auto-compensate)":""}`);if($)console.log(`Video: ${e.video}`);if(e.bgm)console.log(`BGM: ${e.bgm} (ducking: ${e.ducking??r.ducking})`);if(w!=null)console.log(`Patch: caption #${w}`);console.log(`Output: ${S}`);if(w!=null){return _dubPatch(e,c,S,x)}console.log(`\n[1/2] 合成 TTS 音频 (${c.length} 条字幕)...`);const b=[];let T=null;let k=0;for(let e=0;e<c.length;e++){const t=c[e];const o=t.endMs-t.startMs;let n=l;if(v&&t.speakerId&&v[t.speakerId]){n=v[t.speakerId]}let s=await synthesizeCaption(g,h,t.text,n,u,"pcm",e,c.length);k++;T=s.quota;if(p&&s.durationMs>o+f){const r=s.durationMs/o;if(r<=2){const a=Math.min(u*r,2);process.stdout.write(` ↳ Re-synth #${t.id} (${(s.durationMs/1e3).toFixed(1)}s > ${(o/1e3).toFixed(1)}s, speed: ${a.toFixed(2)})...`);s=await synthesizeCaption(g,h,t.text,n,a,"pcm",e,c.length);k++;T=s.quota}else{const a=`Caption #${t.id}: audio too long (${(s.durationMs/1e3).toFixed(1)}s for ${(o/1e3).toFixed(1)}s slot, alpha=${r.toFixed(1)}). Consider shortening text.`;x.push(a);console.log(` ⚠ OVERFLOW: ${a}`);const i=await synthesizeCaption(g,h,t.text,n,2,"pcm",e,c.length);k++;T=i.quota;s=i}}b.push({startMs:t.startMs,endMs:t.endMs,audioBuffer:s.audio})}console.log("\n[2/2] 构建时间轴音频...");const{wav:F,duration:E}=m(b);const _=$?S.replace(/\.[^.]+$/,".wav"):S;const A=s.dirname(_);n.mkdirSync(A,{recursive:true});n.writeFileSync(_,F);const I=_.replace(/\.(wav|mp3|mp4)$/i,".txt");const M=c.map((e=>{const t=e.speakerId?`|${e.speakerId}`:"";const o=v&&e.speakerId&&v[e.speakerId]?`|${v[e.speakerId]}`:"";return`[${e.id}${t}${o}] ${e.text}`})).join("\n\n");n.writeFileSync(I,M,"utf8");const L=$||e.bgm;if(L){const{checkFfmpeg:t,mergeAudioVideo:s,mixWithBgm:a}=o(297);const i=await t();if(!i.available){throw new Error("ffmpeg is required for BGM mixing / video merging. Install it:\n"+" macOS: brew install ffmpeg\n"+" Ubuntu: sudo apt install ffmpeg\n"+" Windows: https://ffmpeg.org/download.html")}let c=_;if(e.bgm){const t=_.replace(".wav","-mixed.wav");console.log(` Mixing BGM (ducking: ${e.ducking??r.ducking})...`);await a(_,e.bgm,t,{ducking:e.ducking??r.ducking});c=t;if(!$){n.copyFileSync(c,_);try{n.unlinkSync(c)}catch{}c=_}}if($){console.log(" Merging with video...");await s(e.video,c,S);try{if(_!==S)n.unlinkSync(_);if(e.bgm){const e=_.replace(".wav","-mixed.wav");if(n.existsSync(e))n.unlinkSync(e)}}catch{}}}console.log(`\n=== Done ===`);console.log(`Output: ${S} (${(n.statSync(S).size/1024).toFixed(1)} KB)`);console.log(`Duration: ${E.toFixed(1)}s`);console.log(`Transcript: ${I}`);console.log(`Captions: ${c.length}`);console.log(`Quota: ${k} used, ${T?.remaining??"?"} remaining`);if(x.length>0){console.log(`\nWarnings (${x.length}):`);for(const e of x){console.log(` ⚠ ${e}`)}}return{outputPath:S,textPath:I,duration:E,quotaUsed:k,segmentCount:c.length,warnings:x}}async function _dubPatch(e,t,o,a){const i=e.patch;const c=e.api;const l=e.token;const p=e.voice||r.voice;const d=e.speed??r.speed;let f=null;if(e.voicesMap){f=parseVoicesMap(s.resolve(e.voicesMap))}const g=t.findIndex((e=>e.id===i));if(g===-1){throw new Error(`Caption #${i} not found in SRT. Available IDs: ${t.map((e=>e.id)).join(", ")}`)}const m=t[g];const w=o.replace(/\.[^.]+$/,".wav");if(!n.existsSync(w)){throw new Error(`Patch mode requires an existing output file. `+`Run a full dub first, then use --patch to update individual captions.`)}let x=p;if(f&&m.speakerId&&f[m.speakerId]){x=f[m.speakerId]}console.log(`\n[Patch] Re-synthesizing caption #${i}: "${m.text.slice(0,40)}..."`);const v=await synthesizeCaption(c,l,m.text,x,d,"pcm",0,1);const S=n.readFileSync(w);const $=S.subarray(44);const y=h(m.startMs);const b=h(m.endMs);$.fill(0,y,Math.min(b,$.length));const T=Math.min(v.audio.length,b-y,$.length-y);if(T>0){v.audio.copy($,y,0,T)}const{wav:k}=u([$],0);n.writeFileSync(w,k);console.log(`\n=== Patch Done ===`);console.log(`Updated: caption #${i} in ${w}`);console.log(`Quota: 1 used, ${v.quota?.remaining??"?"} remaining`);return{outputPath:w,textPath:w.replace(/\.wav$/i,".txt"),duration:$.length/(24e3*2),quotaUsed:1,segmentCount:1,warnings:a}}e.exports={dub:dub,ApiError:l,_test:{parseVoicesMap:parseVoicesMap}}},80:(e,t,o)=>{const n=o(896);const s=o(928);const{NARRATE_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{parseParagraphs:u,buildWav:p,concatAudioBuffers:d,getFileExtension:f}=o(56);const{startSpinner:g}=o(339);function parseScript(e){if(!n.existsSync(e)){throw new Error(`Script file not found: ${e}`)}let t;try{t=JSON.parse(n.readFileSync(e,"utf8"))}catch(e){throw new Error(`Invalid JSON in script file: ${e.message}`)}if(!t.segments||!Array.isArray(t.segments)||t.segments.length===0){throw new Error('Script must have a non-empty "segments" array')}for(let e=0;e<t.segments.length;e++){const o=t.segments[e];if(!o.text||typeof o.text!=="string"||o.text.trim().length===0){throw new Error(`Segment ${e+1} must have a non-empty "text" field`)}}return{segments:t.segments.map((e=>({text:e.text.trim(),voiceId:e.voiceId||undefined,speed:e.speed!=null?Number(e.speed):undefined,volume:e.volume!=null?Number(e.volume):undefined,pitch:e.pitch!=null?Number(e.pitch):undefined}))),silence:t.silence!=null?Number(t.silence):r.silence,output:t.output||undefined}}function stripMarkdown(e){return e.replace(/```[\s\S]*?```/g,"").replace(/`([^`]+)`/g,"$1").replace(/!\[[^\]]*\]\([^)]*\)/g,"").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/^#{1,6}\s+/gm,"").replace(/\*{1,3}([^*]+)\*{1,3}/g,"$1").replace(/_{1,3}([^_]+)_{1,3}/g,"$1").replace(/^[-*_]{3,}\s*$/gm,"").replace(/^>\s?/gm,"").replace(/\n{3,}/g,"\n\n").trim()}async function readStdin(){const e=[];for await(const t of process.stdin){e.push(t)}return Buffer.concat(e).toString("utf8")}async function synthesizeSegment(e,t,o,n,s,r,l,u,p,d){process.stdout.write(` TTS [${p+1}/${d}]...`);const f={text:o,voiceId:n,speed:s??1,volume:r??1,format:u||"pcm"};if(l!=null&&l!==0)f.pitch=l;let g,m;try{({status:g,data:m}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},f))}catch(t){console.log(" FAIL");c(t,e)}if(g!==200||m.code!=="success"){console.log(" FAIL");i(g,m,`TTS segment ${p+1}`)}const h=Buffer.from(m.audio,"base64");const w=u==="mp3"?"MP3":u==="wav"?"WAV":"PCM";console.log(` OK (${(h.length/1024).toFixed(0)} KB ${w})`);return{audio:h,quota:m.quota}}async function narrate(e){const sigintHandler=()=>{console.log("\n\nNarration cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _narrate(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _narrate(e){const t=e.voice||r.voice;const o=e.speed??r.speed;const a=e.format||"pcm";const i=e.api;const c=e.token;let l;let g;let m;let h;if(e.script){const t=parseScript(e.script);l=t.segments;g=e.silence??t.silence;m=e.output||t.output;h=`script: ${e.script} (${l.length} segments)`}else if(e.input){const t=s.resolve(e.input);if(!n.existsSync(t)){throw new Error(`Input file not found: ${t}`)}let o=n.readFileSync(t,"utf8");const a=s.extname(t).toLowerCase();if(a===".md"||a===".markdown"){o=stripMarkdown(o)}const i=u(o);if(i.length===0){throw new Error("No text content found in input file")}l=i.map((e=>({text:e})));g=e.silence??r.silence;m=e.output;h=`file: ${e.input} (${l.length} paragraphs)`}else if(e.text){const t=u(e.text);if(t.length===0){throw new Error("No text content provided")}l=t.map((e=>({text:e})));g=e.silence??r.silence;m=e.output;h=`text: ${e.text.length} chars (${l.length} paragraphs)`}else if(!process.stdin.isTTY){const t=await readStdin();if(!t||t.trim().length===0){throw new Error("No input provided via stdin")}const o=u(t);if(o.length===0){throw new Error("No text content found in stdin input")}l=o.map((e=>({text:e})));g=e.silence??r.silence;h=`stdin (${l.length} paragraphs)`}else{throw new Error("No input provided. Use one of:\n"+" --input <file.txt> Read a text or markdown file\n"+' --text "text" Provide inline text\n'+" --script <file.json> Use a script with per-segment control\n"+' echo "text" | voxflow narrate Pipe from stdin')}const w=f(a);if(!m){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);m=s.resolve(`narration-${e}${w}`)}if(!m.endsWith(w)){m=m.replace(/\.(wav|mp3|pcm)$/i,"")+w}console.log("\n=== VoxFlow Narrate ===");console.log(`Input: ${h}`);console.log(`Voice: ${t}${e.script?" (may be overridden per segment)":""}`);console.log(`Format: ${a==="pcm"?"wav (pcm)":a}`);console.log(`Speed: ${o}`);if(a==="mp3"){console.log(`Output: ${m}`);console.log(` (MP3 模式不插入段间静音)`)}else{console.log(`Silence: ${g}s`);console.log(`Output: ${m}`)}console.log(`\n[1/2] 合成 TTS 音频 (${l.length} 段)...`);const x=[];let v=null;for(let e=0;e<l.length;e++){const n=l[e];const s=await synthesizeSegment(i,c,n.text,n.voiceId||t,n.speed??o,n.volume,n.pitch,a,e,l.length);x.push(s.audio);v=s.quota}console.log("\n[2/2] 拼接音频...");const{audio:S,wav:$,duration:y}=a==="mp3"||a==="wav"?d(x,a,g):p(x,g);const b=S||$;const T=s.dirname(m);n.mkdirSync(T,{recursive:true});n.writeFileSync(m,b);const k=m.replace(/\.(wav|mp3)$/i,".txt");const F=l.map(((e,t)=>{const o=e.voiceId?`[${t+1}|${e.voiceId}]`:`[${t+1}]`;return`${o} ${e.text}`})).join("\n\n");n.writeFileSync(k,F,"utf8");const E=l.length;console.log(`\n=== Done ===`);console.log(`Output: ${m} (${(b.length/1024).toFixed(1)} KB, ${y.toFixed(1)}s)`);console.log(`Transcript: ${k}`);console.log(`Segments: ${l.length}`);console.log(`Quota: ${E} used, ${v?.remaining??"?"} remaining`);return{outputPath:m,textPath:k,duration:y,quotaUsed:E,segmentCount:l.length,format:a}}e.exports={narrate:narrate,ApiError:l,_test:{parseScript:parseScript,stripMarkdown:stripMarkdown}}},35:(e,t,o)=>{const n=o(896);const s=o(928);const{PODCAST_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{buildWav:u}=o(56);const{startSpinner:p}=o(339);function parseDialogueText(e){const t=e.split("\n").filter((e=>e.trim()));const o=[];const n=/^([^::]+)[::]\s*(.+)$/;for(const e of t){const t=e.trim();if(!t)continue;const s=t.match(n);if(s){const e=s[1].trim();const t=s[2].trim();if(t){o.push({speaker:e,text:t})}}else if(t.length>0){o.push({speaker:"旁白",text:t})}}return o}async function generateDialogue(e,t,o){const n=p("\n[1/3] 生成对话文本...");let s,r;try{({status:s,data:r}=await a(`${e}/api/llm/generate-dialogue`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{prompt:o.topic,style:o.style,length:o.length,dialogueMode:true,autoSpeakerNames:true,exchanges:o.exchanges}))}catch(t){n.stop("FAIL");c(t,e)}if(s!==200||r.code!=="success"){n.stop("FAIL");i(s,r,"Dialogue generation")}const l=r.text;const u=r.voiceMapping||{};const d=r.quota;n.stop("OK");const f=parseDialogueText(l);const g=[...new Set(f.map((e=>e.speaker)))];console.log(` ${l.length} 字, ${f.length} 段, ${g.length} 位说话者`);console.log(` 说话者: ${g.join(", ")}`);console.log(` 配额剩余: ${d?.remaining??"?"}`);return{text:l,segments:f,voiceMapping:u,speakers:g,quota:d}}async function synthesizeSegment(e,t,o,n,s,r,l,u){process.stdout.write(` TTS [${r+1}/${l}] ${u}...`);let p,d;try{({status:p,data:d}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{text:o,voiceId:n,speed:s,volume:1}))}catch(t){console.log(" FAIL");c(t,e)}if(p!==200||d.code!=="success"){console.log(" FAIL");i(p,d,`TTS segment ${r+1}`)}const f=Buffer.from(d.audio,"base64");console.log(` OK (${(f.length/1024).toFixed(0)} KB)`);return{pcm:f,quota:d.quota}}async function synthesizeAll(e,t,o,n,s){console.log(`\n[2/3] 合成 TTS 音频 (${o.length} 段, 多角色)...`);const a=[];let i=null;for(let c=0;c<o.length;c++){const l=o[c];const u=n[l.speaker]||{};const p=u.voiceId||r.defaultVoice||"v-female-R2s4N9qJ";const d=u.speed||s;const f=await synthesizeSegment(e,t,l.text,p,d,c,o.length,l.speaker);a.push(f.pcm);i=f.quota}return{pcmBuffers:a,quota:i}}async function podcast(e){const sigintHandler=()=>{console.log("\n\nGeneration cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _podcast(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _podcast(e){const t=e.style||r.style;const o=e.length||r.length;const a=e.exchanges||r.exchanges;const i=e.speed??r.speed;const c=e.silence??r.silence;const l=e.api;const p=e.token;const d=e.topic||"科技领域的最新趋势";let f=e.output;if(!f){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);f=s.resolve(`podcast-${e}.wav`)}console.log("\n=== VoxFlow Podcast Generator ===");console.log(`Topic: ${d}`);console.log(`Style: ${t}`);console.log(`Length: ${o}`);console.log(`Exchanges: ${a}`);console.log(`Speed: ${i}`);console.log(`API: ${l}`);console.log(`Output: ${f}`);const{text:g,segments:m,voiceMapping:h,speakers:w}=await generateDialogue(l,p,{topic:d,style:t,length:o,exchanges:a});if(m.length===0){throw new Error("No dialogue segments found in generated text")}console.log("\n Voice assignments:");for(const e of w){const t=h[e];if(t){console.log(` ${e} → ${t.voiceId}`)}else{console.log(` ${e} → (default)`)}}const{pcmBuffers:x,quota:v}=await synthesizeAll(l,p,m,h,i);console.log("\n[3/3] 拼接音频...");const{wav:S,duration:$}=u(x,c);const y=s.dirname(f);n.mkdirSync(y,{recursive:true});n.writeFileSync(f,S);const b=f.replace(/\.wav$/,".txt");const T=m.map(((e,t)=>`[${t+1}] ${e.speaker}:${e.text}`)).join("\n\n");n.writeFileSync(b,T,"utf8");const k=2+m.length;console.log(`\n=== Done ===`);console.log(`Output: ${f} (${(S.length/1024).toFixed(1)} KB, ${$.toFixed(1)}s)`);console.log(`Script: ${b}`);console.log(`Quota: ${k} used, ${v?.remaining??"?"} remaining`);return{outputPath:f,textPath:b,duration:$,quotaUsed:k}}e.exports={podcast:podcast,ApiError:l,_test:{parseDialogueText:parseDialogueText}}},214:(e,t,o)=>{const n=o(896);const s=o(928);const{STORY_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{parseParagraphs:u,buildWav:p,createSilence:d}=o(56);const{startSpinner:f}=o(339);async function generateStory(e,t,o){const n=f("\n[1/3] 生成故事文本...");let s,r;try{({status:s,data:r}=await a(`${e}/api/llm/chat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{messages:[{role:"user",content:o}],model:"gpt-4o-mini",stream:false,temperature:.8}))}catch(t){n.stop("FAIL");c(t,e)}if(s!==200||r.code!=="success"){n.stop("FAIL");i(s,r,"LLM")}const l=r.content;const u=r.quota;n.stop("OK");console.log(` ${l.length} 字. 配额剩余: ${u?.remaining??"?"}`);return{story:l,quota:u}}async function synthesizeParagraph(e,t,o,n,s,r,l){process.stdout.write(` TTS [${r+1}/${l}]...`);let u,p;try{({status:u,data:p}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{text:o,voiceId:n,speed:s,volume:1}))}catch(t){console.log(" FAIL");c(t,e)}if(u!==200||p.code!=="success"){console.log(" FAIL");i(u,p,`TTS paragraph ${r+1}`)}const d=Buffer.from(p.audio,"base64");console.log(` OK (${(d.length/1024).toFixed(0)} KB)`);return{pcm:d,quota:p.quota}}async function synthesizeAll(e,t,o,n,s){console.log(`\n[2/3] 合成 TTS 音频 (${o.length} 段)...`);const r=[];let a=null;for(let i=0;i<o.length;i++){const c=await synthesizeParagraph(e,t,o[i],n,s,i,o.length);r.push(c.pcm);a=c.quota}return{pcmBuffers:r,quota:a}}async function story(e){const sigintHandler=()=>{console.log("\n\nGeneration cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _story(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _story(e){const t=e.voice||r.voice;const o=e.paragraphs||r.paragraphs;const a=e.speed??r.speed;const i=e.silence??r.silence;const c=e.api;const l=e.token;const d=e.topic||`请写一个适合5岁儿童的短故事,要求:\n1. 分${o}段,每段2-3句话\n2. 每段描述一个清晰的画面场景\n3. 语言简单易懂,充满童趣\n4. 段落之间用空行分隔\n5. 不要添加段落编号,直接输出故事内容`;let f=e.output;if(!f){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);f=s.resolve(`story-${e}.wav`)}console.log("\n=== VoxFlow Story Generator ===");console.log(`Voice: ${t}`);console.log(`API: ${c}`);console.log(`Paragraphs: ${o}`);console.log(`Speed: ${a}`);console.log(`Output: ${f}`);const{story:g}=await generateStory(c,l,d);const m=u(g);if(m.length===0){throw new Error("No paragraphs found in generated story")}console.log(` ${m.length} 段`);const{pcmBuffers:h,quota:w}=await synthesizeAll(c,l,m,t,a);console.log("\n[3/3] 拼接音频...");const{wav:x,duration:v}=p(h,i);const S=s.dirname(f);n.mkdirSync(S,{recursive:true});n.writeFileSync(f,x);const $=f.replace(/\.wav$/,".txt");const y=m.map(((e,t)=>`[${t+1}] ${e}`)).join("\n\n");n.writeFileSync($,y,"utf8");const b=1+m.length;console.log(`\n=== Done ===`);console.log(`Output: ${f} (${(x.length/1024).toFixed(1)} KB, ${v.toFixed(1)}s)`);console.log(`Story: ${$}`);console.log(`Quota: ${b} used, ${w?.remaining??"?"} remaining`);return{outputPath:f,textPath:$,duration:v,quotaUsed:b}}e.exports={story:story,ApiError:l,_test:{parseParagraphs:u,buildWav:p,createSilence:d}}},383:(e,t,o)=>{const n=o(896);const s=o(928);const{SYNTHESIZE_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{buildWav:u,getFileExtension:p}=o(56);const{startSpinner:d}=o(339);async function synthesize(e){const sigintHandler=()=>{console.log("\n\nSynthesis cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _synthesize(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _synthesize(e){const t=e.text;if(!t||t.trim().length===0){throw new Error('No text provided. Usage: voxflow synthesize "your text here"')}const o=e.voice||r.voice;const l=e.speed??r.speed;const f=e.volume??r.volume;const g=e.pitch??r.pitch;const m=e.format||"pcm";const h=e.api;const w=e.token;const x=p(m);let v=e.output;if(!v){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);v=s.resolve(`tts-${e}${x}`)}console.log("\n=== VoxFlow Synthesize ===");console.log(`Voice: ${o}`);console.log(`Format: ${m==="pcm"?"wav (pcm)":m}`);console.log(`Speed: ${l}`);if(f!==1)console.log(`Volume: ${f}`);if(g!==0)console.log(`Pitch: ${g}`);console.log(`Text: ${t.length>60?t.slice(0,57)+"...":t}`);console.log(`Output: ${v}`);const S=d("\n[1/1] 合成 TTS 音频...");let $,y;try{({status:$,data:y}=await a(`${h}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${w}`}},{text:t.trim(),voiceId:o,format:m,speed:l,volume:f,pitch:g}))}catch(e){S.stop("FAIL");c(e,h)}if($!==200||y.code!=="success"){S.stop("FAIL");i($,y,"TTS")}const b=Buffer.from(y.audio,"base64");S.stop("OK");let T,k;if(m==="mp3"){T=b;k=b.length/4e3;console.log(` ${(b.length/1024).toFixed(0)} KB MP3`)}else if(m==="wav"){T=b;const e=b.length>44?b.readUInt32LE(28):48e3;const t=b.length>44?b.readUInt32LE(40):b.length;k=t/e;console.log(` ${(b.length/1024).toFixed(0)} KB WAV`)}else{const e=u([b],0);T=e.wav;k=e.duration;console.log(` ${(b.length/1024).toFixed(0)} KB PCM → WAV`)}const F=s.dirname(v);n.mkdirSync(F,{recursive:true});n.writeFileSync(v,T);const E=1;console.log(`\n=== Done ===`);console.log(`Output: ${v} (${(T.length/1024).toFixed(1)} KB, ${k.toFixed(1)}s)`);console.log(`Quota: ${E} used, ${y.quota?.remaining??"?"} remaining`);return{outputPath:v,duration:k,quotaUsed:E,format:m}}e.exports={synthesize:synthesize,ApiError:l}},585:(e,t,o)=>{const n=o(896);const s=o(928);const{API_BASE:r,TRANSLATE_DEFAULTS:a}=o(782);const{chatCompletion:i,detectLanguage:c}=o(133);const{parseSrt:l,formatSrt:u}=o(813);const p={zh:"Chinese (Simplified)",en:"English",ja:"Japanese",ko:"Korean",fr:"French",de:"German",es:"Spanish",pt:"Portuguese",ru:"Russian",ar:"Arabic",th:"Thai",vi:"Vietnamese",it:"Italian"};function batchCaptions(e,t=10){const o=[];for(let n=0;n<e.length;n+=t){o.push(e.slice(n,n+t))}return o}function buildTranslationPrompt(e,t,o){const n=[`You are a professional subtitle translator. Translate each numbered line from ${t} to ${o}.`,"","Rules:","- Return ONLY the translated lines, one per number","- Keep the exact same numbering (1., 2., 3., ...)","- Preserve [Speaker: xxx] tags unchanged — do NOT translate speaker names","- Keep translations concise and natural for subtitles","- Do not add explanations, notes, or extra text"].join("\n");const s=e.map(((e,t)=>{const o=e.speakerId?`[Speaker: ${e.speakerId}] `:"";return`${t+1}. ${o}${e.text}`})).join("\n");return{system:n,user:s}}function parseTranslationResponse(e,t){const o=e.trim().split("\n").filter((e=>e.trim()));const n=[];for(let e=0;e<t.length;e++){const s=new RegExp(`^${e+1}\\.\\s*(.+)$`);const r=o.find((e=>s.test(e.trim())));if(r){const o=r.trim().replace(s,"$1").trim();let a=o;const i=o.match(/^\[Speaker:\s*[^\]]+\]\s*/i);if(i){a=o.slice(i[0].length)}n.push({...t[e],text:a||t[e].text})}else{if(e<o.length){const s=o[e].replace(/^\d+\.\s*/,"").trim();let r=s;const a=s.match(/^\[Speaker:\s*[^\]]+\]\s*/i);if(a){r=s.slice(a[0].length)}n.push({...t[e],text:r||t[e].text})}else{n.push({...t[e]})}}}return n}function realignTimings(e,t){const o=.3;const n=100;const s=t.map(((t,s)=>{const r=e[s];if(!r)return t;const a=r.text.length;const i=t.text.length;if(a===0)return t;const c=i/a;if(c<1+o&&c>1-o){return t}const l=r.endMs-r.startMs;let u=Math.round(l*c);const p=s<e.length-1?e[s+1].startMs:Infinity;const d=p-t.startMs-n;if(u>d&&d>0){u=d}u=Math.max(u,500);return{...t,endMs:t.startMs+u}}));return s}async function translate(e){const sigintHandler=()=>{console.log("\n\nTranslation cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _translate(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _translate(e){const{token:t,api:o=r,srt:n,text:s,input:i,from:c,to:l,output:u,realign:p=false,batchSize:d=a.batchSize}=e;if(n)return _translateSrt({token:t,api:o,srt:n,from:c,to:l,output:u,realign:p,batchSize:d});if(s)return _translateText({token:t,api:o,text:s,from:c,to:l});if(i)return _translateFile({token:t,api:o,input:i,from:c,to:l,output:u});throw new Error("No input specified. Use --srt, --text, or --input")}async function _translateSrt({token:e,api:t,srt:r,from:c,to:d,output:f,realign:g,batchSize:m}){console.log("\n=== VoxFlow Translate (SRT) ===");const h=s.resolve(r);const w=n.readFileSync(h,"utf8");const x=l(w);if(x.length===0){throw new Error(`SRT file is empty or invalid: ${h}`)}console.log(`Input: ${s.basename(h)}`);console.log(`Captions: ${x.length}`);const v=c||await autoDetectLanguage(t,x);const S=p[v]||v;const $=p[d]||d;console.log(`From: ${S} (${v})`);console.log(`To: ${$} (${d})`);console.log(`Realign: ${g?"yes":"no"}`);const y=batchCaptions(x,m);console.log(`Batches: ${y.length} (batch size: ${m})`);console.log("");let b=[];let T=0;for(let o=0;o<y.length;o++){const n=y[o];process.stdout.write(` [${o+1}/${y.length}] Translating ${n.length} captions...`);const{system:s,user:r}=buildTranslationPrompt(n,S,$);const c=await i({apiBase:t,token:e,messages:[{role:"system",content:s},{role:"user",content:r}],temperature:a.temperature,maxTokens:a.maxTokens});const l=parseTranslationResponse(c.content,n);b=b.concat(l);T++;if(c.quota){console.log(` OK (remaining: ${c.quota.remaining})`)}else{console.log(" OK")}}if(g){console.log(" Re-aligning subtitle timing...");b=realignTimings(x,b)}b=b.map(((e,t)=>({...e,id:t+1})));const k=u(b);let F;if(f){F=s.resolve(f)}else{const e=s.basename(h,s.extname(h));const t=s.dirname(h);F=o.ab+"cli/"+t+"/"+e+"-"+d+".srt"}n.writeFileSync(F,k,"utf8");console.log(`\n=== Done ===`);console.log(`Output: ${F}`);console.log(`Captions: ${b.length}`);console.log(`Quota: ${T} used`);if(b.length>0){console.log(`\n--- Preview ---`);const e=b.slice(0,3);for(const t of e){const e=t.speakerId?`[${t.speakerId}] `:"";const o=t.text.length>60?t.text.slice(0,57)+"...":t.text;console.log(` ${t.id}. ${e}${o}`)}if(b.length>3){console.log(` ... (${b.length-3} more)`)}}return{outputPath:F,captionCount:b.length,quotaUsed:T,from:v,to:d}}async function _translateText({token:e,api:t,text:o,from:n,to:s}){console.log("\n=== VoxFlow Translate (Text) ===");const r=n||await autoDetectLanguage(t,[{text:o}]);const c=p[r]||r;const l=p[s]||s;console.log(`From: ${c} → To: ${l}`);const u=await i({apiBase:t,token:e,messages:[{role:"system",content:`You are a professional translator. Translate the following text from ${c} to ${l}. Return ONLY the translation, no explanations.`},{role:"user",content:o}],temperature:a.temperature,maxTokens:a.maxTokens});const d=u.content.trim();console.log(`\n${d}`);const f=u.quota?u.quota.remaining:"?";console.log(`\n(Quota: 1 used, ${f} remaining)`);return{text:d,quotaUsed:1,from:r,to:s}}async function _translateFile({token:e,api:t,input:r,from:c,to:l,output:u}){console.log("\n=== VoxFlow Translate (File) ===");const d=s.resolve(r);const f=n.readFileSync(d,"utf8");if(f.trim().length===0){throw new Error(`Input file is empty: ${d}`)}console.log(`Input: ${s.basename(d)}`);console.log(`Length: ${f.length} chars`);const g=c||await autoDetectLanguage(t,[{text:f}]);const m=p[g]||g;const h=p[l]||l;console.log(`From: ${m} → To: ${h}`);const w=await i({apiBase:t,token:e,messages:[{role:"system",content:`You are a professional translator. Translate the following document from ${m} to ${h}. Preserve the original formatting (paragraphs, line breaks, markdown). Return ONLY the translation.`},{role:"user",content:f}],temperature:a.temperature,maxTokens:Math.max(a.maxTokens,4e3)});const x=w.content.trim();let v;if(u){v=s.resolve(u)}else{const e=s.extname(d);const t=s.basename(d,e);const n=s.dirname(d);v=o.ab+"cli/"+n+"/"+t+"-"+l+""+e}n.writeFileSync(v,x+"\n","utf8");const S=w.quota?w.quota.remaining:"?";console.log(`\n=== Done ===`);console.log(`Output: ${v}`);console.log(`Quota: 1 used, ${S} remaining`);return{outputPath:v,quotaUsed:1,from:g,to:l}}async function autoDetectLanguage(e,t){const o=t.slice(0,3).map((e=>e.text)).join(" ");const n=await c({apiBase:e,text:o});return n||"auto"}e.exports={translate:translate,LANG_MAP:p,_test:{buildTranslationPrompt:buildTranslationPrompt,parseTranslationResponse:parseTranslationResponse,realignTimings:realignTimings,batchCaptions:batchCaptions}}},863:(e,t,o)=>{const n=o(896);const s=o(928);const r=o(857);const{checkFfmpeg:a,extractAudio:i}=o(297);const{asr:c}=o(929);const{translate:l}=o(585);const{dub:u}=o(944);const{detectLanguage:p}=o(133);const{parseSrt:d}=o(813);const{API_BASE:f,VIDEO_TRANSLATE_DEFAULTS:g}=o(782);const m={zh:"16k_zh",en:"16k_en",ja:"16k_ja",ko:"16k_ko","zh-en":"16k_zh_en"};function resolveAsrLang(e,t){if(t)return t;if(e&&m[e])return m[e];return"16k_zh"}async function videoTranslate(e){const sigintHandler=()=>{console.log("\n\nVideo translation cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _videoTranslate(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _videoTranslate(e){const{token:t,api:m=f,input:h,from:w,to:x,voice:v,voicesMap:S,realign:$=false,output:y,keepIntermediates:b=false,batchSize:T=g.batchSize,speed:k=g.speed,asrMode:F,asrLang:E}=e;const _=s.resolve(h);const A=s.basename(_,s.extname(_));console.log("\n=== VoxFlow Video Translate ===");console.log(`Input: ${s.basename(_)}`);console.log(`Target: ${x}`);console.log("");const I=n.mkdtempSync(s.join(r.tmpdir(),"voxflow-vtranslate-"));let M=0;const L={};try{process.stdout.write("[1/4] Checking FFmpeg... ");const e=await a();if(!e.available){throw new Error("FFmpeg is required for video-translate. Install: https://ffmpeg.org/download.html")}console.log(`OK (${e.version})`);process.stdout.write("[2/4] Transcribing audio... ");const r=s.join(I,"extracted-audio.wav");await i(_,r);const f=s.join(I,"source.srt");const g=resolveAsrLang(w,E);const h={token:t,api:m,input:r,format:"srt",output:f,lang:g};if(F)h.mode=F;const C=await c(h);if(C.captionCount===0){throw new Error("ASR produced no captions. The video may have no audible speech.")}L.asr={mode:C.mode,duration:C.duration,captionCount:C.captionCount,quotaUsed:C.quotaUsed};M+=C.quotaUsed;console.log(`${C.captionCount} captions (${C.mode} mode)`);let N=w;if(!N){const e=n.readFileSync(f,"utf8");const t=d(e);const o=t.slice(0,3).map((e=>e.text)).join(" ");N=await p({apiBase:m,text:o})||"auto"}process.stdout.write(`[3/4] Translating (${N} → ${x})... `);const P=s.join(I,`translated-${x}.srt`);const O=await l({token:t,api:m,srt:f,from:N,to:x,output:P,realign:$,batchSize:T});L.translate={from:O.from,to:O.to,captionCount:O.captionCount,quotaUsed:O.quotaUsed};M+=O.quotaUsed;console.log(`${O.captionCount} captions translated`);process.stdout.write("[4/4] Dubbing and merging video... ");const D=y?s.resolve(y):o.ab+"cli/"+s.dirname(_)+"/"+A+"-"+x+".mp4";const q=await u({token:t,api:m,srt:P,voice:v,voicesMap:S,speed:k,video:_,output:D});L.dub={segmentCount:q.segmentCount,duration:q.duration,quotaUsed:q.quotaUsed,warnings:q.warnings};M+=q.quotaUsed;console.log(`${q.segmentCount} segments dubbed`);if(b){const e=s.resolve(s.dirname(D),`${A}-${x}-intermediates`);n.mkdirSync(e,{recursive:true});const t=[["extracted-audio.wav",r],["source.srt",f],[`translated-${x}.srt`,P]];for(const[o,r]of t){if(n.existsSync(r)){n.copyFileSync(r,s.join(e,o))}}console.log(`\nIntermediates saved: ${e}`)}console.log("\n=== Done ===");console.log(`Output: ${D}`);console.log(`Language: ${N} → ${x}`);console.log(`Captions: ${O.captionCount}`);console.log(`Duration: ${q.duration.toFixed(1)}s`);console.log(`Quota: ${M} used`);if(L.dub.warnings&&L.dub.warnings.length>0){console.log(`\nWarnings:`);for(const e of L.dub.warnings){console.log(` - ${e}`)}}return{outputPath:D,from:N,to:x,captionCount:O.captionCount,quotaUsed:M,stages:L}}finally{if(!b){try{n.rmSync(I,{recursive:true,force:true})}catch{}}}}e.exports={videoTranslate:videoTranslate}},784:(e,t,o)=>{const{request:n,throwNetworkError:s}=o(852);async function voices(e){const t=e.api;const o=e.extended?"true":"false";let r,a;try{({status:r,data:a}=await n(`${t}/api/tts/voices?includeExtended=${o}`,{method:"GET"}))}catch(e){s(e,t)}if(r!==200){throw new Error(`Failed to fetch voices (${r}): ${a?.message||"unknown error"}`)}let i=a.voices||a.data?.voices||[];if(e.gender){const t=normalizeGender(e.gender);if(!t){console.error(`Error: --gender must be one of: male, m, female, f (got: "${e.gender}")`);process.exit(1)}i=i.filter((e=>{const o=(e.gender||"").toLowerCase();return o===t}))}if(e.language){const t=e.language.toLowerCase();i=i.filter((e=>(e.language||"").toLowerCase()===t))}if(e.search){const t=e.search.toLowerCase();i=i.filter((e=>{const o=[e.name,e.nameEn,e.tone,e.style,e.description,e.scenarios].filter(Boolean).join(" ").toLowerCase();return o.includes(t)}))}if(i.length===0){console.log("No voices match your criteria.");return}if(e.json){console.log(JSON.stringify(i,null,2))}else{printTable(i)}console.log(`\nFound ${i.length} voice${i.length===1?"":"s"}.`)}function normalizeGender(e){const t=(e||"").toLowerCase().trim();if(t==="male"||t==="m")return"male";if(t==="female"||t==="f")return"female";return null}function printTable(e){const t=24;const o=14;const n=8;const s=22;const r=20;const a=["ID".padEnd(t),"Name".padEnd(o),"Gender".padEnd(n),"Tone".padEnd(s),"Style".padEnd(r)].join(" ");console.log(`\n${a}`);console.log("-".repeat(a.length));for(const a of e){const e=[truncate(a.id||"",t).padEnd(t),truncate(a.name||"",o).padEnd(o),truncate(a.gender||"",n).padEnd(n),truncate(a.tone||"",s).padEnd(s),truncate(a.style||"",r).padEnd(r)].join(" ");console.log(e)}}function truncate(e,t){if(e.length<=t)return e;return e.slice(0,t-1)+"…"}e.exports={voices:voices}},514:(e,t,o)=>{const n=o(896);const{request:s,throwApiError:r,throwNetworkError:a,ApiError:i}=o(852);const c=6e4;const l=72e5;const u=5*1024*1024;const p=3e3;const d=3e5;const f={WAITING:0,PROCESSING:1,SUCCESS:2,FAILED:3};function detectMode(e,t,o){if(e<=c&&o<=u){return"sentence"}if(e<=l&&t){return"flash"}return"file"}function authHeaders(e){return{"Content-Type":"application/json",Authorization:`Bearer ${e}`}}async function recognizeSentence(e){const{apiBase:t,token:o,url:c,filePath:l,lang:u="16k_zh",wordInfo:p=false}=e;const d={EngSerViceType:u,VoiceFormat:"wav",SubServiceType:2,WordInfo:p?1:0,ConvertNumMode:1};if(c){d.Url=c;d.SourceType=0}else if(l){const e=n.readFileSync(l);d.Data=e.toString("base64");d.DataLen=e.length;d.SourceType=1}else{throw new Error("Either url or filePath is required for sentence recognition")}try{const{status:e,data:n}=await s(`${t}/api/asr/sentence`,{method:"POST",headers:authHeaders(o)},d);if(e!==200||n.code!=="success"){r(e,n,"ASR sentence")}return{result:n.result,audioTime:n.audioTime,wordList:n.wordList||[],requestId:n.requestId,quota:n.quota}}catch(e){if(e instanceof i)throw e;a(e,t)}}async function recognizeFlash(e){const{apiBase:t,token:o,url:n,lang:c="16k_zh",speakerDiarization:l=false,speakerNumber:u=0}=e;if(!n){throw new Error("Flash recognition requires a URL (cannot use base64 data)")}const p={engine_type:c,voice_format:"wav",url:n,speaker_diarization:l?1:0,speaker_number:u,filter_dirty:0,filter_modal:0,filter_punc:0,convert_num_mode:1,word_info:1,first_channel_only:1};try{const{status:e,data:n}=await s(`${t}/api/asr/flash`,{method:"POST",headers:authHeaders(o)},p);if(e!==200||n.code!=="success"){r(e,n,"ASR flash")}return{flashResult:n.flash_result||[],audioDuration:n.audio_duration||0,requestId:n.request_id,quota:n.quota}}catch(e){if(e instanceof i)throw e;a(e,t)}}async function submitFileTask(e){const{apiBase:t,token:o,url:c,filePath:l,lang:p="16k_zh",speakerDiarization:d=false,speakerNumber:f=0}=e;const g={EngineModelType:p,ChannelNum:1,ResTextFormat:0,FilterDirty:0,FilterModal:0,FilterPunc:0,ConvertNumMode:1,SpeakerDiarization:d?1:0,SpeakerNumber:f};if(c){g.Url=c;g.SourceType=0}else if(l){const e=n.readFileSync(l);if(e.length>u){throw new Error(`File too large for base64 upload (${(e.length/1024/1024).toFixed(1)} MB). `+"Upload to COS first or use flash mode with a URL.")}g.Data=e.toString("base64");g.DataLen=e.length;g.SourceType=1}else{throw new Error("Either url or filePath is required for file recognition")}try{const{status:e,data:n}=await s(`${t}/api/asr/file`,{method:"POST",headers:authHeaders(o)},g);if(e!==200||n.code!=="success"){r(e,n,"ASR file submit")}return{taskId:n.taskId,requestId:n.requestId,quota:n.quota}}catch(e){if(e instanceof i)throw e;a(e,t)}}async function pollTaskResult(e){const{apiBase:t,token:o,taskId:n,pollIntervalMs:c=p,pollTimeoutMs:l=d,onProgress:u}=e;const g=Date.now();while(true){const e=Date.now()-g;if(e>l){throw new Error(`ASR task ${n} timed out after ${Math.round(e/1e3)}s. `+"The task may still complete — check later with: voxflow asr --task-id "+n)}try{const{status:a,data:i}=await s(`${t}/api/asr/result/${n}`,{method:"GET",headers:authHeaders(o)});if(a!==200||i.code!=="success"){r(a,i,"ASR poll")}const c=i.data;const l=c.Status;if(u)u(l,e);if(l===f.SUCCESS){return{result:c.Result,audioTime:c.AudioTime,status:l}}if(l===f.FAILED){throw new Error(`ASR task ${n} failed: ${c.Result||"Unknown error"}`)}}catch(o){if(o instanceof i)throw o;if(e+c<l){}else{a(o,t)}}await sleep(c)}}async function recognize(e){const{mode:t="auto",url:o,filePath:n,durationMs:s,fileSize:r=0}=e;const a=!!o;const i=t==="auto"?detectMode(s,a,r):t;switch(i){case"sentence":{const t=await recognizeSentence(e);return{mode:"sentence",result:t.result,audioTime:t.audioTime,wordList:t.wordList,quota:t.quota}}case"flash":{if(!o){throw new Error("Flash mode requires a URL. Upload the file to COS first, or use --mode auto.")}const t=await recognizeFlash(e);const n=(t.flashResult||[]).flatMap((e=>e.sentence_list?e.sentence_list.map((e=>e.text)):[e.text])).join("");return{mode:"flash",result:n,flashResult:t.flashResult,audioDuration:t.audioDuration,audioTime:(t.audioDuration||0)/1e3,quota:t.quota}}case"file":{const t=await submitFileTask(e);const o=await pollTaskResult({apiBase:e.apiBase,token:e.token,taskId:t.taskId,onProgress:e.onProgress});return{mode:"file",result:o.result,audioTime:o.audioTime,taskId:t.taskId,quota:t.quota}}default:throw new Error(`Unknown ASR mode: ${i}. Use: auto, sentence, flash, or file`)}}function sleep(e){return new Promise((t=>setTimeout(t,e)))}e.exports={recognize:recognize,recognizeSentence:recognizeSentence,recognizeFlash:recognizeFlash,submitFileTask:submitFileTask,pollTaskResult:pollTaskResult,detectMode:detectMode,SENTENCE_MAX_MS:c,FLASH_MAX_MS:l,BASE64_MAX_BYTES:u,TASK_STATUS:f}},388:(e,t,o)=>{const{execFile:n}=o(317);const s=o(928);const r=o(857);const a=o(896);function runCommand(e,t,o){return new Promise(((s,r)=>{n(e,t,{timeout:6e5,...o},((e,t,o)=>{if(e){e.stderr=o;e.stdout=t;r(e)}else{s({stdout:t,stderr:o})}}))}))}async function getMediaInfo(e){const t=s.resolve(e);if(!a.existsSync(t)){throw new Error(`File not found: ${t}`)}try{const{stdout:e}=await runCommand("ffprobe",["-v","error","-show_entries","format=duration","-show_entries","stream=codec_type,codec_name,sample_rate,channels","-of","json",t]);const o=JSON.parse(e);const n=o.streams||[];const s=o.format||{};const r=n.find((e=>e.codec_type==="audio"));const a=n.find((e=>e.codec_type==="video"));const i=parseFloat(s.duration);const c=isNaN(i)?0:Math.round(i*1e3);return{durationMs:c,hasVideo:!!a,hasAudio:!!r,audioCodec:r?r.codec_name:null,sampleRate:r?parseInt(r.sample_rate,10):null,channels:r?parseInt(r.channels,10):null}}catch(t){if(t.code==="ENOENT"){throw new Error("ffprobe not found. Please install ffmpeg:\n"+" macOS: brew install ffmpeg\n"+" Ubuntu: sudo apt install ffmpeg\n"+" Windows: https://ffmpeg.org/download.html")}throw new Error(`Failed to probe media file ${e}: ${t.message}`)}}async function extractAudioForAsr(e,t={}){const o=s.resolve(e);if(!a.existsSync(o)){throw new Error(`File not found: ${o}`)}const n=t.outputDir||r.tmpdir();const i=s.basename(o,s.extname(o));const c=s.join(n,`asr-${i}-${Date.now()}.wav`);try{await runCommand("ffmpeg",["-i",o,"-vn","-acodec","pcm_s16le","-ar","16000","-ac","1","-y",c])}catch(t){if(t.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg:\n"+" macOS: brew install ffmpeg\n"+" Ubuntu: sudo apt install ffmpeg\n"+" Windows: https://ffmpeg.org/download.html")}throw new Error(`Failed to extract audio from ${e}: ${t.stderr||t.message}`)}const l=a.statSync(c);const u=Math.round((l.size-44)/32);return{wavPath:c,durationMs:u,needsCleanup:true}}async function needsConversion(e){try{const t=await getMediaInfo(e);if(t.hasVideo)return true;if(t.audioCodec!=="pcm_s16le")return true;if(t.sampleRate!==16e3)return true;if(t.channels!==1)return true;return false}catch{return true}}e.exports={getMediaInfo:getMediaInfo,extractAudioForAsr:extractAudioForAsr,needsConversion:needsConversion}},56:e=>{function parseParagraphs(e){const t=e.split(/\n\s*\n/).map((e=>e.replace(/^\d+[.、)\]]\s*/,"").trim())).filter((e=>e.length>0));return t}function createSilence(e,t){const o=Math.floor(t*e);return Buffer.alloc(o*2,0)}function buildWav(e,t){const o=24e3;const n=16;const s=1;const r=n/8;const a=s*r;const i=o*a;const c=createSilence(t,o);let l=0;for(let t=0;t<e.length;t++){l+=e[t].length;if(t<e.length-1){l+=c.length}}const u=Buffer.alloc(44);u.write("RIFF",0);u.writeUInt32LE(36+l,4);u.write("WAVE",8);u.write("fmt ",12);u.writeUInt32LE(16,16);u.writeUInt16LE(1,20);u.writeUInt16LE(s,22);u.writeUInt32LE(o,24);u.writeUInt32LE(i,28);u.writeUInt16LE(a,32);u.writeUInt16LE(n,34);u.write("data",36);u.writeUInt32LE(l,40);const p=[u];for(let t=0;t<e.length;t++){p.push(e[t]);if(t<e.length-1){p.push(c)}}return{wav:Buffer.concat(p),duration:l/i}}function getFileExtension(e){switch(e){case"mp3":return".mp3";case"wav":return".wav";case"pcm":default:return".wav"}}function concatAudioBuffers(e,t,o){if(t==="mp3"){const t=Buffer.concat(e);const o=t.length/4e3;return{audio:t,duration:o}}if(t==="wav"){const t=e.map(extractPcmFromWav);return buildWav(t,o)}return buildWav(e,o)}function extractPcmFromWav(e){const t=Buffer.from("data");let o=12;while(o<e.length-8){if(e.subarray(o,o+4).equals(t)){const t=e.readUInt32LE(o+4);return e.subarray(o+8,o+8+t)}const n=e.readUInt32LE(o+4);o+=8+n}return e.subarray(44)}e.exports={parseParagraphs:parseParagraphs,createSilence:createSilence,buildWav:buildWav,concatAudioBuffers:concatAudioBuffers,getFileExtension:getFileExtension}},986:(e,t,o)=>{const n=o(611);const s=o(896);const r=o(928);const a=o(982);const i=o(785);const{TOKEN_PATH:c,getConfigDir:l,LOGIN_PAGE:u,AUTH_TIMEOUT_MS:p,API_BASE:d}=o(782);function readCachedToken(){try{const e=s.readFileSync(c,"utf8");const t=JSON.parse(e);if(!t.access_token)return null;const o=decodeJwtPayload(t.access_token);if(!o||!o.exp)return null;const n=Math.floor(Date.now()/1e3);if(o.exp-n<300)return null;return t}catch{return null}}function writeCachedToken(e){const t=l();s.mkdirSync(t,{recursive:true,mode:448});s.writeFileSync(c,JSON.stringify(e,null,2),{encoding:"utf8",mode:384})}function clearToken(){try{s.unlinkSync(c)}catch{}}function decodeJwtPayload(e){try{const t=e.split(".");if(t.length!==3)return null;const o=t[1].replace(/-/g,"+").replace(/_/g,"/");return JSON.parse(Buffer.from(o,"base64").toString("utf8"))}catch{return null}}async function getToken({api:e,force:t}={}){if(!t){const t=readCachedToken();if(t){const o=!e||e===t.api;if(o)return t.access_token}}return browserLogin(e||d)}function getTokenInfo(){const e=readCachedToken();if(!e)return null;const t=decodeJwtPayload(e.access_token);if(!t)return null;const o=Math.floor(Date.now()/1e3);return{email:t.email||e.email||"(unknown)",expiresAt:new Date(t.exp*1e3).toISOString(),remaining:t.exp-o,valid:t.exp-o>300,api:e.api||d}}function browserLogin(e){return new Promise(((t,s)=>{const r=a.randomBytes(16).toString("hex");let c=false;let l=null;function settle(o){if(c)return;c=true;const n=decodeJwtPayload(o);writeCachedToken({access_token:o,expires_at:n?.exp||0,email:n?.email||"",api:e,cached_at:(new Date).toISOString()});if(l){l.close();l=null}d.close();t(o)}const d=n.createServer(((e,t)=>{const o=new URL(e.url,`http://127.0.0.1`);if(o.pathname!=="/callback"){t.writeHead(404,{"Content-Type":"text/plain"});t.end("Not Found");return}const n=o.searchParams.get("token");const s=o.searchParams.get("state");if(s!==r){t.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});t.end("<h1>认证失败</h1><p>state 参数不匹配,请重试。</p>");return}if(!n){t.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});t.end("<h1>认证失败</h1><p>未收到 token,请重试。</p>");return}t.writeHead(200,{"Content-Type":"text/html; charset=utf-8"});t.end(`<!DOCTYPE html>\n<html><head><meta charset="utf-8"><title>登录成功</title></head>\n<body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f0fdf4">\n<div style="text-align:center">\n<h1 style="color:#16a34a;font-size:2rem">登录成功</h1>\n<p style="color:#666;margin-top:0.5rem">已授权 voxflow CLI,可以关闭此窗口。</p>\n</div></body></html>`);settle(n)}));d.listen(0,"127.0.0.1",(async()=>{const e=d.address().port;const t=`${u}?state=${r}&callback_port=${e}`;console.log("\n[33m🔐 需要登录。正在打开浏览器...[0m");console.log(` 若未自动打开: ${t}\n`);let n=false;try{const e=(await o.e(935).then(o.bind(o,935))).default;const s=await e(t);if(s&&typeof s.on==="function"){s.on("error",(()=>{n=true;console.log("[31m 浏览器打开失败,请手动复制上面的链接到浏览器。[0m\n");startStdinListener()}))}}catch{n=true;console.log("[31m 浏览器打开失败,请手动复制上面的链接到浏览器。[0m\n");startStdinListener()}function startStdinListener(){if(c||l||!process.stdin.isTTY)return;console.log(" [36m登录后网页会显示授权码,粘贴到此处回车即可[0m");l=i.createInterface({input:process.stdin,output:process.stdout,terminal:false});process.stdout.write(" > Token: ");l.on("line",(e=>{const t=e.trim();if(!t)return;const o=decodeJwtPayload(t);if(!o){console.log(" [31m无效的 token,请重新粘贴完整的授权码。[0m");process.stdout.write(" > Token: ");return}const n=Math.floor(Date.now()/1e3);if(o.exp&&o.exp<n){console.log(" [31mtoken 已过期,请重新登录获取。[0m");process.stdout.write(" > Token: ");return}console.log(`\n[32m✓ 授权成功 (${o.email||"user"})[0m`);settle(t)}))}}));const f=setTimeout((()=>{if(!c){c=true;if(l){l.close();l=null}d.close();s(new Error(`登录超时 (${p/1e3}s)。请重试: voxflow login`))}}),p);d.on("close",(()=>clearTimeout(f)));d.on("error",(e=>{if(!c){c=true;if(l){l.close();l=null}s(new Error(`本地服务器启动失败: ${e.message}`))}}))}))}e.exports={getToken:getToken,clearToken:clearToken,getTokenInfo:getTokenInfo}},782:(e,t,o)=>{const n=o(928);const s=o(857);const r="https://api.voxflow.studio";const a="https://iwkonytsjysszmafqchh.supabase.co";const i="sb_publishable_TEh6H4K9OWXUNfWSeBKXlQ_hg7Zzm6b";const c="voxflow";function getConfigDir(){if(process.platform==="win32"){return n.join(process.env.APPDATA||n.join(s.homedir(),"AppData","Roaming"),c)}const e=process.env.XDG_CONFIG_HOME||n.join(s.homedir(),".config");return n.join(e,c)}const l=n.join(getConfigDir(),"token.json");const u={voice:"v-female-R2s4N9qJ",paragraphs:5,speed:1,silence:.8};const p={template:"interview",exchanges:8,length:"medium",style:"professional",speakers:2,silence:.5,speed:1};const d={voice:"v-female-R2s4N9qJ",speed:1,volume:1,pitch:0};const f={voice:"v-female-R2s4N9qJ",speed:1,silence:.8};const g={voice:"v-female-R2s4N9qJ",speed:1,toleranceMs:50,ducking:.2};const m={lang:"16k_zh",mode:"auto",format:"srt",pollIntervalMs:3e3,pollTimeoutMs:3e5,engine:"auto",model:"base"};const h={batchSize:10,temperature:.3,maxTokens:2e3};const w={batchSize:10,speed:1};const x="https://voxflow.studio";const v=`${x}/app/cli-auth.html`;const S=18e4;e.exports={API_BASE:r,WEB_BASE:x,SUPABASE_URL:a,SUPABASE_ANON_KEY:i,TOKEN_PATH:l,getConfigDir:getConfigDir,DEFAULTS:u,STORY_DEFAULTS:u,PODCAST_DEFAULTS:p,SYNTHESIZE_DEFAULTS:d,NARRATE_DEFAULTS:f,DUB_DEFAULTS:g,ASR_DEFAULTS:m,TRANSLATE_DEFAULTS:h,VIDEO_TRANSLATE_DEFAULTS:w,LOGIN_PAGE:v,AUTH_TIMEOUT_MS:S}},567:(e,t,o)=>{const n=o(896);const s=o(928);const r=o(611);const a=o(692);const{request:i,throwApiError:c,throwNetworkError:l,ApiError:u}=o(852);const p={".wav":"audio/wav",".mp3":"audio/mpeg",".ogg":"audio/ogg",".m4a":"audio/x-m4a",".mp4":"video/mp4",".webm":"video/webm",".mov":"video/quicktime",".avi":"video/x-msvideo",".mkv":"video/x-matroska",".flac":"audio/flac"};function getMimeType(e){const t=s.extname(e).toLowerCase();return p[t]||"application/octet-stream"}async function uploadFileToCos(e,t,o){const r=s.resolve(e);if(!n.existsSync(r)){throw new Error(`File not found: ${r}`)}const a=n.statSync(r);const p=s.basename(r);const d=getMimeType(r);const f=a.size;let g;try{const{status:e,data:n}=await i(`${t}/api/file-upload/get-upload-url`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`}},{filename:p,fileType:d,fileSize:f});if(e!==200||n.code!=="success"){c(e,n,"Get upload URL")}g=n.data}catch(e){if(e instanceof u)throw e;l(e,t)}const{uploadUrl:m,key:h,bucket:w,region:x}=g;await putFile(m,r,d);let v;try{v=await getSignedDownloadUrl(t,o,h)}catch{v=`https://${w}.cos.${x}.myqcloud.com/${h}`}return{cosUrl:v,key:h}}function putFile(e,t,o){return new Promise(((s,i)=>{const c=new URL(e);const l=c.protocol==="https:"?a:r;const u=n.statSync(t).size;const p={hostname:c.hostname,port:c.port||(c.protocol==="https:"?443:80),path:c.pathname+c.search,method:"PUT",headers:{"Content-Type":o,"Content-Length":u}};const d=l.request(p,(e=>{const t=[];e.on("data",(e=>t.push(e)));e.on("end",(()=>{if(e.statusCode>=200&&e.statusCode<300){s()}else{const o=Buffer.concat(t).toString("utf8");i(new Error(`COS upload failed (${e.statusCode}): ${o.slice(0,300)}`))}}))}));d.on("error",(e=>i(new Error(`COS upload network error: ${e.message}`))));d.setTimeout(3e5,(()=>{d.destroy();i(new Error("COS upload timeout (5 min)"))}));const f=n.createReadStream(t);f.pipe(d);f.on("error",(e=>{d.destroy();i(new Error(`Failed to read file for upload: ${e.message}`))}))}))}async function getSignedDownloadUrl(e,t,o){const{status:n,data:s}=await i(`${e}/api/file-upload/get-download-url`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{key:o});if(n!==200||s.code!=="success"){throw new Error(`Failed to get download URL: ${s.message||n}`)}return s.data.downloadUrl}e.exports={uploadFileToCos:uploadFileToCos,getSignedDownloadUrl:getSignedDownloadUrl,getMimeType:getMimeType}},297:(e,t,o)=>{const{execFile:n}=o(317);const s=o(928);const r=o(896);function runCommand(e,t,o){return new Promise(((s,r)=>{n(e,t,{timeout:3e5,...o},((e,t,o)=>{if(e){e.stderr=o;e.stdout=t;r(e)}else{s({stdout:t,stderr:o})}}))}))}async function checkFfmpeg(){try{const{stdout:e}=await runCommand("ffmpeg",["-version"]);const t=e.match(/ffmpeg version (\S+)/);const o=t?t[1]:"unknown";let n=false;try{await runCommand("ffprobe",["-version"]);n=true}catch{}return{available:true,version:o,ffprobeAvailable:n}}catch{return{available:false}}}async function getAudioDuration(e){const t=s.resolve(e);try{const{stdout:e}=await runCommand("ffprobe",["-v","error","-show_entries","format=duration","-of","default=noprint_wrappers=1:nokey=1",t]);const o=parseFloat(e.trim());if(isNaN(o)){throw new Error(`Could not parse duration from ffprobe output: "${e.trim()}"`)}return Math.round(o*1e3)}catch(t){if(t.code==="ENOENT"){throw new Error("ffprobe not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to get duration of ${e}: ${t.message}`)}}async function extractAudio(e,t){const o=s.resolve(e);const n=s.resolve(t);try{await runCommand("ffmpeg",["-i",o,"-vn","-acodec","pcm_s16le","-ar","24000","-ac","1","-y",n]);return n}catch(t){if(t.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to extract audio from ${e}: ${t.stderr||t.message}`)}}async function mergeAudioVideo(e,t,o){const n=s.resolve(e);const r=s.resolve(t);const a=s.resolve(o);try{await runCommand("ffmpeg",["-i",n,"-i",r,"-c:v","copy","-map","0:v:0","-map","1:a:0","-shortest","-y",a]);return a}catch(e){if(e.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to merge audio/video: ${e.stderr||e.message}`)}}async function mixWithBgm(e,t,o,n={}){const r=n.ducking??.2;const a=s.resolve(e);const i=s.resolve(t);const c=s.resolve(o);try{await runCommand("ffmpeg",["-i",a,"-i",i,"-filter_complex",`[1:a]volume=${r}[bgm_low];`+`[0:a][bgm_low]amix=inputs=2:duration=first:dropout_transition=2[out]`,"-map","[out]","-acodec","pcm_s16le","-ar","24000","-ac","1","-y",c]);return c}catch(e){if(e.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to mix audio with BGM: ${e.stderr||e.message}`)}}async function warnIfMissingFfmpeg(e,t){const o=await checkFfmpeg();if(o.available)return o;const n=s.join(e,".ffmpeg-hint-shown");try{if(r.existsSync(n))return o}catch{}const a={dub:"video merging (--video), BGM mixing (--bgm), speed adjustment (--speed-auto)",asr:"audio format conversion, video audio extraction"};const i=a[t]||"audio/video processing";console.log("\n[33m"+`[hint] ffmpeg not found — needed for ${i}.\n`+" Install: brew install ffmpeg (macOS) / sudo apt install ffmpeg (Linux)\n"+" Without ffmpeg, some features will be unavailable.[0m\n");try{r.mkdirSync(e,{recursive:true});r.writeFileSync(n,(new Date).toISOString(),"utf8")}catch{}return o}e.exports={checkFfmpeg:checkFfmpeg,getAudioDuration:getAudioDuration,extractAudio:extractAudio,mergeAudioVideo:mergeAudioVideo,mixWithBgm:mixWithBgm,warnIfMissingFfmpeg:warnIfMissingFfmpeg}},852:(e,t,o)=>{const n=o(611);const s=o(692);class ApiError extends Error{constructor(e,t,o){super(e);this.name="ApiError";this.code=t;this.status=o}}function throwApiError(e,t,o){if(e===401){throw new ApiError(`Token expired or invalid. Run: voxflow login`,"token_expired",401)}if(e===429||t&&t.code==="quota_exceeded"){throw new ApiError(`Daily quota exceeded. Your quota resets tomorrow. Check: voxflow status`,"quota_exceeded",429)}if(e>=500){throw new ApiError(`Server error (${e}). Please try again later.`,"server_error",e)}const n=t?.message||t?.code||JSON.stringify(t);throw new ApiError(`${o} failed (${e}): ${n}`,"api_error",e)}function throwNetworkError(e,t){const o=e.code||"";if(o==="ECONNREFUSED"||o==="ENOTFOUND"||o==="ETIMEDOUT"){throw new ApiError(`Cannot reach API server at ${t}. Check your internet connection or try --api <url>`,"network_error",0)}throw e}function request(e,t,o){return new Promise(((r,a)=>{const i=new URL(e);const c=i.protocol==="https:"?s:n;const l=c.request(i,t,(e=>{const t=[];e.on("data",(e=>t.push(e)));e.on("end",(()=>{const o=Buffer.concat(t).toString("utf8");try{r({status:e.statusCode,data:JSON.parse(o)})}catch{a(new Error(`Non-JSON response (${e.statusCode}): ${o.slice(0,200)}`))}}))}));l.on("error",(e=>a(e)));l.setTimeout(6e4,(()=>{l.destroy();a(new Error("Request timeout (60s)"))}));if(o)l.write(JSON.stringify(o));l.end()}))}e.exports={request:request,ApiError:ApiError,throwApiError:throwApiError,throwNetworkError:throwNetworkError}},133:(e,t,o)=>{const{request:n,throwApiError:s,throwNetworkError:r}=o(852);async function chatCompletion({apiBase:e,token:t,messages:o,temperature:a=.3,maxTokens:i=2e3}){let c,l;try{({status:c,data:l}=await n(`${e}/api/llm/chat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{messages:o,temperature:a,max_tokens:i}))}catch(t){r(t,e)}if(c!==200||l.code!=="success"){s(c,l,"LLM chat")}return{content:l.content,usage:l.usage,quota:l.quota}}async function detectLanguage({apiBase:e,text:t}){let o,s;try{({status:o,data:s}=await n(`${e}/api/lang-detect/detect`,{method:"POST",headers:{"Content-Type":"application/json"}},{text:t.slice(0,200)}))}catch{return"auto"}if(o===200&&s.code==="success"){return s.language}return"auto"}e.exports={chatCompletion:chatCompletion,detectLanguage:detectLanguage}},384:(e,t,o)=>{const{spawn:n}=o(317);const s=o(928);const r=o(857);const a=o(896);async function checkRecAvailable(){return new Promise((e=>{const t=n("rec",["--version"],{stdio:"pipe"});let o="";t.stdout.on("data",(e=>{o+=e}));t.stderr.on("data",(e=>{o+=e}));t.on("error",(()=>{e({available:false,error:"rec (sox) not found. Please install sox:\n"+" macOS: brew install sox\n"+" Ubuntu: sudo apt install sox\n"+" Windows: https://sourceforge.net/projects/sox/"})}));t.on("close",(t=>{e({available:t===0||o.length>0})}))}))}function recordMic(e={}){const{outputDir:t=r.tmpdir(),maxSeconds:o=300,silenceThreshold:i=0}=e;const c=s.join(t,`mic-${Date.now()}.wav`);return new Promise(((e,t)=>{const s=["-r","16000","-c","1","-b","16","-e","signed-integer",c,"trim","0",String(o)];if(i>0){s.push("silence","1","0.1","1%","1",String(i),"1%")}const r=n("rec",s,{stdio:["pipe","pipe","pipe"]});let l="";r.stderr.on("data",(e=>{l+=e.toString()}));r.on("error",(e=>{if(e.code==="ENOENT"){t(new Error("rec (sox) not found. Please install sox:\n"+" macOS: brew install sox\n"+" Ubuntu: sudo apt install sox\n"+" Windows: https://sourceforge.net/projects/sox/"))}else{t(new Error(`Microphone recording failed: ${e.message}`))}}));let u="timeout";r.on("close",(o=>{if(!a.existsSync(c)){return t(new Error(`Recording failed — no output file created.\n${l.slice(0,500)}`))}const n=a.statSync(c);if(n.size<100){a.unlinkSync(c);return t(new Error("Recording produced an empty file. Check that your microphone is connected and accessible."))}const s=Math.round((n.size-44)/32);e({wavPath:c,durationMs:s,stopped:u})}));const stopRecording=()=>{u="user";r.kill("SIGTERM")};if(process.stdin.isTTY){process.stdin.setRawMode(true);process.stdin.resume();const onKey=e=>{if(e[0]===13||e[0]===10||e[0]===113){u="user";process.stdin.setRawMode(false);process.stdin.removeListener("data",onKey);process.stdin.pause();r.kill("SIGTERM")}if(e[0]===3){u="user";process.stdin.setRawMode(false);process.stdin.removeListener("data",onKey);process.stdin.pause();r.kill("SIGTERM")}};process.stdin.on("data",onKey);r.on("close",(()=>{try{process.stdin.removeListener("data",onKey);if(process.stdin.isTTY){process.stdin.setRawMode(false)}process.stdin.pause()}catch{}}))}r._stopRecording=stopRecording}))}e.exports={recordMic:recordMic,checkRecAvailable:checkRecAvailable}},339:e=>{function startSpinner(e){const t=["|","/","-","\\"];let o=0;process.stdout.write(e+" "+t[0]);const n=setInterval((()=>{o=(o+1)%t.length;process.stdout.write("\b"+t[o])}),120);return{stop(e){clearInterval(n);process.stdout.write("\b"+e+"\n")}}}e.exports={startSpinner:startSpinner}},813:e=>{function parseTimestamp(e){const t=e.trim().match(/^(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})$/);if(!t){throw new Error(`Invalid SRT timestamp: "${e}"`)}const[,o,n,s,r]=t;return parseInt(o,10)*36e5+parseInt(n,10)*6e4+parseInt(s,10)*1e3+parseInt(r,10)}function formatTimestamp(e){if(e<0)e=0;const t=Math.floor(e/36e5);e%=36e5;const o=Math.floor(e/6e4);e%=6e4;const n=Math.floor(e/1e3);const s=e%1e3;return String(t).padStart(2,"0")+":"+String(o).padStart(2,"0")+":"+String(n).padStart(2,"0")+","+String(s).padStart(3,"0")}function parseSrt(e){if(!e||e.trim().length===0){return[]}const t=[];const o=e.replace(/\r\n/g,"\n").replace(/\r/g,"\n");const n=o.split(/\n\s*\n/).filter((e=>e.trim().length>0));for(const e of n){const o=e.trim().split("\n");if(o.length<2)continue;let n=0;let s;const r=o[0].trim();if(/^\d+$/.test(r)){s=parseInt(r,10);n=1}else{s=t.length+1}if(n>=o.length)continue;const a=o[n].trim();const i=a.match(/^(\d{1,2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{1,2}:\d{2}:\d{2}[,.]\d{3})/);if(!i)continue;const c=parseTimestamp(i[1]);const l=parseTimestamp(i[2]);n++;const u=o.slice(n).filter((e=>e.trim().length>0));if(u.length===0)continue;const p=u.join("\n");let d;let f=p;const g=p.match(/^\[Speaker:\s*([^\]]+)\]\s*/i);if(g){d=g[1].trim();f=p.slice(g[0].length)}if(f.trim().length===0)continue;t.push({id:s,startMs:c,endMs:l,text:f.trim(),...d?{speakerId:d}:{}})}t.sort(((e,t)=>e.startMs-t.startMs));return t}function formatSrt(e){return e.map(((e,t)=>{const o=e.id||t+1;const n=formatTimestamp(e.startMs);const s=formatTimestamp(e.endMs);const r=e.speakerId?`[Speaker: ${e.speakerId}] `:"";return`${o}\n${n} --\x3e ${s}\n${r}${e.text}`})).join("\n\n")+"\n"}function buildCaptionsFromFlash(e){const t=[];let o=1;for(const n of e){const e=n.sentence_list||[];for(const n of e){const e={id:o++,startMs:n.start_time||0,endMs:n.end_time||0,text:(n.text||"").trim()};if(n.speaker_id!==undefined&&n.speaker_id!==null){e.speakerId=`Speaker${n.speaker_id}`}if(e.text.length>0){t.push(e)}}}return t}function buildCaptionsFromSentence(e,t,o){if(!e||e.trim().length===0)return[];if(o&&o.length>0){return buildCaptionsFromWordList(o,e)}return[{id:1,startMs:0,endMs:Math.round(t*1e3),text:e.trim()}]}function buildCaptionsFromWordList(e,t){if(!e||e.length===0){return t?[{id:1,startMs:0,endMs:0,text:t}]:[]}const o=500;const n=5e3;const s=15e3;const r=/[.!?。!?…]+$/;const getWord=e=>e.word||e.Word||"";const getStart=e=>e.startTime??e.StartTime??0;const getEnd=e=>e.endTime??e.EndTime??0;const a=e.slice(0,10).map(getWord).join("");const i=(a.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g)||[]).length;const c=i<a.length*.3;const l=c?" ":"";const u=[];let p=[];let d=getStart(e[0]);let f=d;function flushCaption(){if(p.length===0)return;const e=p.join(l).trim();if(e.length>0){u.push({id:u.length+1,startMs:d,endMs:f,text:e})}p=[]}for(let t=0;t<e.length;t++){const a=e[t];const i=getStart(a);const c=getEnd(a);const l=i-f;const u=i-d;if(l>o&&p.length>0){flushCaption();d=i}else if(p.length>0&&u>n&&r.test(p[p.length-1])){flushCaption();d=i}else if(p.length>0&&u>s){flushCaption();d=i}p.push(getWord(a));f=c||f}flushCaption();return u}function buildCaptionsFromFile(e,t){if(!e||e.trim().length===0)return[];if(/^\d+\s*\n\d{2}:\d{2}:\d{2}[,.]\d{3}\s*-->/.test(e.trim())){return parseSrt(e)}return[{id:1,startMs:0,endMs:Math.round(t*1e3),text:e.trim()}]}function formatPlainText(e,t={}){return e.map((e=>{const o=t.includeSpeakers&&e.speakerId?`[${e.speakerId}] `:"";return`${o}${e.text}`})).join("\n")+"\n"}function formatJson(e){return JSON.stringify(e,null,2)+"\n"}e.exports={parseSrt:parseSrt,formatSrt:formatSrt,parseTimestamp:parseTimestamp,formatTimestamp:formatTimestamp,buildCaptionsFromFlash:buildCaptionsFromFlash,buildCaptionsFromSentence:buildCaptionsFromSentence,buildCaptionsFromWordList:buildCaptionsFromWordList,buildCaptionsFromFile:buildCaptionsFromFile,formatPlainText:formatPlainText,formatJson:formatJson}},907:(e,t,o)=>{const{createSilence:n,buildWav:s}=o(56);const r=24e3;const a=2;const i=r*a/1e3;function msToBytes(e){const t=Math.round(e*i);return t-t%a}function buildTimelinePcm(e,t){if(!e||e.length===0){return{pcm:Buffer.alloc(0),durationMs:0}}const o=Math.max(...e.map((e=>e.endMs)));const n=t||o;const s=msToBytes(n);const r=Buffer.alloc(s,0);for(const t of e){const e=msToBytes(t.startMs);const o=msToBytes(t.endMs)-e;const n=Math.min(t.audioBuffer.length,o,s-e);if(n>0&&e<s){t.audioBuffer.copy(r,e,0,n)}}return{pcm:r,durationMs:n}}function buildTimelineAudio(e,t){const{pcm:o,durationMs:n}=buildTimelinePcm(e,t);if(o.length===0){return{wav:Buffer.alloc(0),duration:0}}const{wav:r}=s([o],0);return{wav:r,duration:n/1e3}}e.exports={buildTimelinePcm:buildTimelinePcm,buildTimelineAudio:buildTimelineAudio,msToBytes:msToBytes,SAMPLE_RATE:r,BYTES_PER_SAMPLE:a,BYTES_PER_MS:i}},126:(e,t,o)=>{const n=o(896);const s=o(928);const{execFileSync:r}=o(317);const a={"16k_zh":"Chinese","16k_en":"English","16k_zh_en":"Chinese","16k_ja":"Japanese","16k_ko":"Korean","16k_zh_dialect":"Chinese","8k_zh":"Chinese","8k_en":"English",zh:"Chinese",en:"English",ja:"Japanese",ko:"Korean",auto:"auto"};function checkWhisperAvailable(){try{const e=resolveWhisperModule();if(e){return{available:true}}return{available:false,error:"nodejs-whisper is not installed.\n"+"Install it with: npm install -g nodejs-whisper\n"+"Then download a model: npx nodejs-whisper download"}}catch{return{available:false,error:"nodejs-whisper is not installed.\n"+"Install it with: npm install -g nodejs-whisper\n"+"Then download a model: npx nodejs-whisper download"}}}function resolveWhisperModule(){try{return require.resolve("nodejs-whisper")}catch{}try{const e=r("npm",["root","-g"],{encoding:"utf8"}).trim();const t=s.join(e,"nodejs-whisper");if(n.existsSync(t)){return t}}catch{}return null}function loadWhisperModule(){const e=resolveWhisperModule();if(!e){throw new Error("nodejs-whisper is not installed.\n"+"Install: npm install -g nodejs-whisper\n"+"Then: npx nodejs-whisper download")}const t=require(e);return t.nodewhisper||t.default||t}async function transcribeLocal(e,t={}){const{model:o="base",lang:s="16k_zh"}=t;if(!n.existsSync(e)){throw new Error(`WAV file not found: ${e}`)}const r=loadWhisperModule();const i=a[s]||a["auto"]||"auto";const c=o;await r(e,{modelName:c,autoDownloadModelName:c,removeWavFileAfterTranscription:false,whisperOptions:{outputInJson:true,outputInSrt:false,outputInVtt:false,outputInTxt:false,wordTimestamps:true,splitOnWord:true,language:i==="auto"?undefined:i}});const l=e+".json";if(!n.existsSync(l)){const t=e.replace(/\.wav$/i,"");const o=[t+".json",e+".json"];const s=o.find((e=>n.existsSync(e)));if(!s){throw new Error("Whisper completed but no JSON output found.\n"+`Expected: ${l}\n`+"Ensure nodejs-whisper is correctly installed.")}}const u=n.readFileSync(l,"utf8");const p=JSON.parse(u);const d=p.transcription||p.segments||[];const f=parseWhisperOutput(d);cleanupWhisperFiles(e);return f}function parseWhisperOutput(e){if(!e||!Array.isArray(e))return[];let t=0;const o=[];for(const n of e){const e=(n.text||"").trim();if(!e)continue;t++;let s=0;let r=0;if(n.timestamps){s=parseTimestamp(n.timestamps.from);r=parseTimestamp(n.timestamps.to)}else if(n.offsets){s=n.offsets.from||0;r=n.offsets.to||0}else if(typeof n.start==="number"){s=Math.round(n.start*1e3);r=Math.round(n.end*1e3)}o.push({id:t,startMs:s,endMs:r,text:e})}return o}function parseTimestamp(e){if(!e||typeof e!=="string")return 0;const t=e.match(/^(\d+):(\d+):(\d+)\.(\d+)$/);if(!t)return 0;const o=parseInt(t[1],10);const n=parseInt(t[2],10);const s=parseInt(t[3],10);const r=parseInt(t[4].padEnd(3,"0").slice(0,3),10);return(o*3600+n*60+s)*1e3+r}function cleanupWhisperFiles(e){const t=[".json",".srt",".vtt",".txt",".lrc",".wts"];for(const o of t){const t=e+o;try{if(n.existsSync(t)){n.unlinkSync(t)}}catch{}}}e.exports={checkWhisperAvailable:checkWhisperAvailable,transcribeLocal:transcribeLocal,parseWhisperOutput:parseWhisperOutput,parseTimestamp:parseTimestamp,LANG_MAP:a}},317:e=>{"use strict";e.exports=require("child_process")},982:e=>{"use strict";e.exports=require("crypto")},896:e=>{"use strict";e.exports=require("fs")},611:e=>{"use strict";e.exports=require("http")},692:e=>{"use strict";e.exports=require("https")},573:e=>{"use strict";e.exports=require("node:buffer")},421:e=>{"use strict";e.exports=require("node:child_process")},24:e=>{"use strict";e.exports=require("node:fs")},455:e=>{"use strict";e.exports=require("node:fs/promises")},161:e=>{"use strict";e.exports=require("node:os")},760:e=>{"use strict";e.exports=require("node:path")},708:e=>{"use strict";e.exports=require("node:process")},136:e=>{"use strict";e.exports=require("node:url")},975:e=>{"use strict";e.exports=require("node:util")},857:e=>{"use strict";e.exports=require("os")},928:e=>{"use strict";e.exports=require("path")},785:e=>{"use strict";e.exports=require("readline")},330:e=>{"use strict";e.exports=JSON.parse('{"name":"voxflow","version":"1.5.1","description":"AI audio content creation CLI — stories, podcasts, narration, dubbing, transcription, translation, and video translation with TTS","bin":{"voxflow":"./dist/index.js"},"files":["dist/index.js","dist/935.index.js","README.md"],"engines":{"node":">=18.0.0"},"dependencies":{"open":"^10.0.0"},"keywords":["tts","story","podcast","ai","audio","text-to-speech","voice","narration","dubbing","synthesize","voices","document","translate","subtitle","srt","transcribe","asr","video-translate","video","voxflow"],"scripts":{"build":"ncc build bin/voxflow.js -o dist --minify","prepublishOnly":"npm run build","test":"node --test tests/*.test.js"},"author":"gonghaoran","license":"UNLICENSED","homepage":"https://voxflow.studio","repository":{"type":"git","url":"https://github.com/VoxFlowStudio/FlowStudio","directory":"cli"},"publishConfig":{"access":"public"},"devDependencies":{"@vercel/ncc":"^0.38.4"}}')}};var t={};function __nccwpck_require__(o){var n=t[o];if(n!==undefined){return n.exports}var s=t[o]={exports:{}};var r=true;try{e[o](s,s.exports,__nccwpck_require__);r=false}finally{if(r)delete t[o]}return s.exports}__nccwpck_require__.m=e;(()=>{__nccwpck_require__.d=(e,t)=>{for(var o in t){if(__nccwpck_require__.o(t,o)&&!__nccwpck_require__.o(e,o)){Object.defineProperty(e,o,{enumerable:true,get:t[o]})}}}})();(()=>{__nccwpck_require__.f={};__nccwpck_require__.e=e=>Promise.all(Object.keys(__nccwpck_require__.f).reduce(((t,o)=>{__nccwpck_require__.f[o](e,t);return t}),[]))})();(()=>{__nccwpck_require__.u=e=>""+e+".index.js"})();(()=>{__nccwpck_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})();(()=>{__nccwpck_require__.r=e=>{if(typeof Symbol!=="undefined"&&Symbol.toStringTag){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}Object.defineProperty(e,"__esModule",{value:true})}})();if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";(()=>{var e={792:1};var installChunk=t=>{var o=t.modules,n=t.ids,s=t.runtime;for(var r in o){if(__nccwpck_require__.o(o,r)){__nccwpck_require__.m[r]=o[r]}}if(s)s(__nccwpck_require__);for(var a=0;a<n.length;a++)e[n[a]]=1};__nccwpck_require__.f.require=(t,o)=>{if(!e[t]){if(true){installChunk(require("./"+__nccwpck_require__.u(t)))}else e[t]=1}}})();var o={};const{run:n}=__nccwpck_require__(6);n().catch((e=>{console.error(`\nFatal error: ${e.message}`);process.exit(1)}));module.exports=o})();
|
|
2
|
+
(()=>{var e={6:(e,t,o)=>{const{getToken:n,clearToken:s,getTokenInfo:r}=o(986);const{story:a,ApiError:i}=o(214);const{podcast:c,ApiError:l}=o(35);const{synthesize:u}=o(383);const{narrate:p}=o(80);const{voices:d}=o(784);const{dub:f}=o(944);const{asr:g,ASR_DEFAULTS:m}=o(929);const{translate:h}=o(585);const{videoTranslate:w}=o(863);const{warnIfMissingFfmpeg:x}=o(297);const{API_BASE:v,WEB_BASE:S,STORY_DEFAULTS:$,PODCAST_DEFAULTS:y,SYNTHESIZE_DEFAULTS:b,NARRATE_DEFAULTS:T,DUB_DEFAULTS:k,ASR_DEFAULTS:F,TRANSLATE_DEFAULTS:E,VIDEO_TRANSLATE_DEFAULTS:_,getConfigDir:A}=o(782);const I=o(330);const M=i;async function run(e){const t=e||process.argv.slice(2);const o=t[0];if(!o||o==="--help"||o==="-h"){printHelp();return}if(o==="--version"||o==="-v"){console.log(I.version);return}if(t.includes("--help")||t.includes("-h")){printHelp();return}switch(o){case"login":return handleLogin(t.slice(1));case"logout":return handleLogout();case"status":return handleStatus();case"story":case"generate":return handleStory(t.slice(1));case"podcast":return handlePodcast(t.slice(1));case"synthesize":case"say":return handleSynthesize(t.slice(1));case"narrate":return handleNarrate(t.slice(1));case"voices":return handleVoices(t.slice(1));case"dub":return handleDub(t.slice(1));case"asr":case"transcribe":return handleAsr(t.slice(1));case"translate":return handleTranslate(t.slice(1));case"video-translate":return handleVideoTranslate(t.slice(1));case"dashboard":return handleDashboard();default:console.error(`Unknown command: ${o}\nRun voxflow --help for usage.`);process.exit(1)}}async function handleLogin(e){const t=parseFlag(e,"--api")||v;console.log("Logging in...");const o=await n({api:t,force:true});const s=r();if(s){console.log(`\n[32mLogged in as ${s.email}[0m`);console.log(`Token expires: ${s.expiresAt}`);console.log(`API: ${s.api}`)}}function handleLogout(){s();console.log("Logged out. Token cache cleared.")}function handleStatus(){const e=r();if(!e){console.log("Not logged in. Run: voxflow login");return}console.log(`Email: ${e.email}`);console.log(`API: ${e.api}`);console.log(`Expires: ${e.expiresAt}`);console.log(`Valid: ${e.valid?"[32myes[0m":"[31mexpired[0m"}`);if(!e.valid){console.log("\nToken expired. Run: voxflow login")}console.log(`\nDashboard: ${S}/app/`);console.log("Run [36mvoxflow dashboard[0m to open in browser.")}async function handleStory(e){const t=parseFlag(e,"--api")||v;const o=parseFlag(e,"--token");let s;if(o){s=o}else{s=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseIntFlag(e,"--paragraphs");const c=parseFloatFlag(e,"--speed");const l=parseFloatFlag(e,"--silence");const u=parseFlag(e,"--output");if(i!==undefined){if(isNaN(i)||i<1||i>20){console.error(`Error: --paragraphs must be an integer between 1 and 20 (got: "${parseFlag(e,"--paragraphs")}")`);process.exit(1)}}validateSpeed(e,c);validateSilence(e,l);validateOutput(u);const p={token:s,api:t,topic:parseFlag(e,"--topic"),voice:parseFlag(e,"--voice"),output:u,paragraphs:i,speed:c,silence:l};await runWithRetry(a,p,t,o)}async function handlePodcast(e){const t=parseFlag(e,"--api")||v;const o=parseFlag(e,"--token");let s;if(o){s=o}else{s=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const a=parseIntFlag(e,"--exchanges");const i=parseFloatFlag(e,"--speed");const l=parseFloatFlag(e,"--silence");const u=parseFlag(e,"--output");if(a!==undefined){if(isNaN(a)||a<2||a>30){console.error(`Error: --exchanges must be an integer between 2 and 30 (got: "${parseFlag(e,"--exchanges")}")`);process.exit(1)}}validateSpeed(e,i);validateSilence(e,l);validateOutput(u);const p=parseFlag(e,"--length");if(p&&!["short","medium","long"].includes(p)){console.error(`Error: --length must be one of: short, medium, long (got: "${p}")`);process.exit(1)}const d={token:s,api:t,topic:parseFlag(e,"--topic"),style:parseFlag(e,"--style"),length:p,exchanges:a,output:u,speed:i,silence:l};await runWithRetry(c,d,t,o)}async function handleSynthesize(e){const t=parseFlag(e,"--api")||v;const o=parseFlag(e,"--token");let s;if(o){s=o}else{s=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}let a=parseFlag(e,"--text");if(!a){const t=new Set(["--text","--voice","--speed","--volume","--pitch","--output","--token","--api","--format"]);for(let o=0;o<e.length;o++){if(e[o].startsWith("--")){if(t.has(e[o]))o++;continue}a=e[o];break}}if(!a){console.error('Error: No text provided. Usage: voxflow synthesize "your text here"');process.exit(1)}const i=parseFloatFlag(e,"--speed");const c=parseFloatFlag(e,"--volume");const l=parseFloatFlag(e,"--pitch");const p=parseFlag(e,"--output");const d=parseFlag(e,"--format");validateSpeed(e,i);validateOutput(p,d);validateFormat(d);if(c!==undefined){if(isNaN(c)||c<.1||c>2){console.error(`Error: --volume must be between 0.1 and 2.0 (got: "${parseFlag(e,"--volume")}")`);process.exit(1)}}if(l!==undefined){if(isNaN(l)||l<-12||l>12){console.error(`Error: --pitch must be between -12 and 12 (got: "${parseFlag(e,"--pitch")}")`);process.exit(1)}}const f={token:s,api:t,text:a,voice:parseFlag(e,"--voice"),output:p,speed:i,volume:c,pitch:l,format:d||undefined};await runWithRetry(u,f,t,o)}async function handleNarrate(e){const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--input");const c=parseFlag(e,"--text");const l=parseFlag(e,"--script");const u=parseFloatFlag(e,"--speed");const d=parseFloatFlag(e,"--silence");const f=parseFlag(e,"--output");const g=parseFlag(e,"--format");validateSpeed(e,u);validateSilence(e,d);validateOutput(f,g);validateFormat(g);if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: Input file not found: ${n}`);process.exit(1)}}if(l){const e=o(896);const t=o(928);const n=t.resolve(l);if(!e.existsSync(n)){console.error(`Error: Script file not found: ${n}`);process.exit(1)}}const m={token:a,api:t,input:i,text:c,script:l,voice:parseFlag(e,"--voice"),output:f,speed:u,silence:d,format:g||undefined};await runWithRetry(p,m,t,s)}async function handleVoices(e){const t=parseFlag(e,"--api")||v;const o={api:t,search:parseFlag(e,"--search"),gender:parseFlag(e,"--gender"),language:parseFlag(e,"--language"),json:parseBoolFlag(e,"--json"),extended:parseBoolFlag(e,"--extended")};await d(o)}async function handleDub(e){await x(A(),"dub");const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--srt");const c=parseFlag(e,"--video");const l=parseFlag(e,"--output");const u=parseFloatFlag(e,"--speed");const p=parseFloatFlag(e,"--ducking");const d=parseIntFlag(e,"--patch");if(!i&&!parseBoolFlag(e,"--help")){console.error("Error: --srt <file> is required. Usage: voxflow dub --srt <file.srt>");process.exit(1)}if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: SRT file not found: ${n}`);process.exit(1)}}if(c){const e=o(896);const t=o(928);const n=t.resolve(c);if(!e.existsSync(n)){console.error(`Error: Video file not found: ${n}`);process.exit(1)}}const g=parseFlag(e,"--voices");if(g){const e=o(896);const t=o(928);const n=t.resolve(g);if(!e.existsSync(n)){console.error(`Error: Voices map file not found: ${n}`);process.exit(1)}}const m=parseFlag(e,"--bgm");if(m){const e=o(896);const t=o(928);const n=t.resolve(m);if(!e.existsSync(n)){console.error(`Error: BGM file not found: ${n}`);process.exit(1)}}validateSpeed(e,u);if(l){const e=c?[".mp4",".mkv",".mov"]:[".wav",".mp3"];const t=e.some((e=>l.toLowerCase().endsWith(e)));if(!t){const t=e.join(", ");console.error(`Error: --output path must end with ${t}`);process.exit(1)}}if(p!==undefined){if(isNaN(p)||p<0||p>1){console.error(`Error: --ducking must be between 0 and 1.0 (got: "${parseFlag(e,"--ducking")}")`);process.exit(1)}}const h={token:a,api:t,srt:i,video:c,output:l,speed:u,patch:d,voice:parseFlag(e,"--voice"),voicesMap:g,speedAuto:parseBoolFlag(e,"--speed-auto"),bgm:m,ducking:p};await runWithRetry(f,h,t,s)}async function handleAsr(e){await x(A(),"asr");const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");const a=parseFlag(e,"--engine")||F.engine;const i=parseFlag(e,"--model")||F.model;if(a&&!["auto","local","cloud","whisper","tencent"].includes(a)){console.error(`Error: --engine must be one of: auto, local, cloud (got: "${a}")`);process.exit(1)}if(i&&!["tiny","base","small","medium","large"].includes(i)){console.error(`Error: --model must be one of: tiny, base, small, medium, large (got: "${i}")`);process.exit(1)}const c=a==="local"||a==="whisper";let l;if(c){l=null}else if(s){l=s}else{l=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const u=parseFlag(e,"--input");const p=parseFlag(e,"--url");const d=parseBoolFlag(e,"--mic");const f=parseFlag(e,"--mode")||m.mode;const h=parseFlag(e,"--lang")||parseFlag(e,"--language")||m.lang;const w=parseFlag(e,"--format")||m.format;const S=parseFlag(e,"--output");const $=parseBoolFlag(e,"--speakers");const y=parseIntFlag(e,"--speaker-number");const b=parseIntFlag(e,"--task-id");if(f&&!["auto","sentence","flash","file"].includes(f)){console.error(`Error: --mode must be one of: auto, sentence, flash, file (got: "${f}")`);process.exit(1)}if(w&&!["srt","txt","json"].includes(w)){console.error(`Error: --format must be one of: srt, txt, json (got: "${w}")`);process.exit(1)}if(u){const e=o(896);const t=o(928);const n=t.resolve(u);if(!e.existsSync(n)){console.error(`Error: Input file not found: ${n}`);process.exit(1)}}const T={token:l,api:t,input:u,url:p,mic:d,mode:f,lang:h,format:w,output:S,speakers:$,speakerNumber:y,taskId:b,engine:a,model:i};if(c){await g(T)}else{await runWithRetry(g,T,t,s)}}async function handleTranslate(e){const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--srt");const c=parseFlag(e,"--text");const l=parseFlag(e,"--input");const u=parseFlag(e,"--from");const p=parseFlag(e,"--to");const d=parseFlag(e,"--output");const f=parseBoolFlag(e,"--realign");const g=parseIntFlag(e,"--batch-size");if(!p&&!parseBoolFlag(e,"--help")){console.error("Error: --to <lang> is required. Example: voxflow translate --srt file.srt --to en");process.exit(1)}const m=["zh","en","ja","ko","fr","de","es","pt","ru","ar","th","vi","it"];if(p&&!m.includes(p)){console.error(`Error: --to must be one of: ${m.join(", ")} (got: "${p}")`);process.exit(1)}if(u&&!m.includes(u)&&u!=="auto"){console.error(`Error: --from must be one of: auto, ${m.join(", ")} (got: "${u}")`);process.exit(1)}const w=[i,c,l].filter(Boolean).length;if(w===0&&!parseBoolFlag(e,"--help")){console.error("Error: Provide one of: --srt <file>, --text <text>, --input <file>");process.exit(1)}if(w>1){console.error("Error: Specify only one input: --srt, --text, or --input");process.exit(1)}if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: SRT file not found: ${n}`);process.exit(1)}}if(l){const e=o(896);const t=o(928);const n=t.resolve(l);if(!e.existsSync(n)){console.error(`Error: Input file not found: ${n}`);process.exit(1)}}if(g!==undefined){if(isNaN(g)||g<1||g>20){console.error(`Error: --batch-size must be between 1 and 20 (got: "${parseFlag(e,"--batch-size")}")`);process.exit(1)}}const x={token:a,api:t,srt:i,text:c,input:l,from:u,to:p,output:d,realign:f,batchSize:g};await runWithRetry(h,x,t,s)}async function handleVideoTranslate(e){if(parseBoolFlag(e,"--help")||parseBoolFlag(e,"-h")){printHelp();return}const t=parseFlag(e,"--api")||v;const s=parseFlag(e,"--token");let a;if(s){a=s}else{a=await n({api:t});const e=r();if(e){console.log(`[32mLogged in as ${e.email}[0m`)}}const i=parseFlag(e,"--input");const c=parseFlag(e,"--from");const l=parseFlag(e,"--to");const u=parseFlag(e,"--voice");const p=parseFlag(e,"--voices");const d=parseFlag(e,"--output");const f=parseBoolFlag(e,"--realign");const g=parseBoolFlag(e,"--keep-intermediates");const m=parseIntFlag(e,"--batch-size");const h=parseFloatFlag(e,"--speed");const x=parseFlag(e,"--asr-mode");const S=parseFlag(e,"--asr-lang");if(!i){console.error("Error: --input <video-file> is required. Example: voxflow video-translate --input video.mp4 --to en");process.exit(1)}if(!l){console.error("Error: --to <lang> is required. Example: voxflow video-translate --input video.mp4 --to en");process.exit(1)}const $=["zh","en","ja","ko","fr","de","es","pt","ru","ar","th","vi","it"];if(l&&!$.includes(l)){console.error(`Error: --to must be one of: ${$.join(", ")} (got: "${l}")`);process.exit(1)}if(c&&!$.includes(c)&&c!=="auto"){console.error(`Error: --from must be one of: auto, ${$.join(", ")} (got: "${c}")`);process.exit(1)}if(i){const e=o(896);const t=o(928);const n=t.resolve(i);if(!e.existsSync(n)){console.error(`Error: Video file not found: ${n}`);process.exit(1)}}if(h!==undefined&&(isNaN(h)||h<.5||h>2)){console.error(`Error: --speed must be between 0.5 and 2.0 (got: "${parseFlag(e,"--speed")}")`);process.exit(1)}if(m!==undefined&&(isNaN(m)||m<1||m>20)){console.error(`Error: --batch-size must be between 1 and 20 (got: "${parseFlag(e,"--batch-size")}")`);process.exit(1)}const y=["auto","sentence","flash","file"];if(x&&!y.includes(x)){console.error(`Error: --asr-mode must be one of: ${y.join(", ")} (got: "${x}")`);process.exit(1)}if(p){const e=o(896);const t=o(928);const n=t.resolve(p);if(!e.existsSync(n)){console.error(`Error: Voices map file not found: ${n}`);process.exit(1)}}const b={token:a,api:t,input:i,from:c,to:l,voice:u,voicesMap:p,output:d,realign:f,keepIntermediates:g,batchSize:m,speed:h,asrMode:x,asrLang:S};await runWithRetry(w,b,t,s)}async function handleDashboard(){const e=`${S}/app/`;console.log(`\nOpening dashboard: ${e}`);try{const t=(await o.e(935).then(o.bind(o,935))).default;const n=await t(e);if(n&&typeof n.on==="function"){n.on("error",(()=>{console.log("[31mFailed to open browser. Visit manually:[0m");console.log(` ${e}`)}))}}catch{console.log("[31mFailed to open browser. Visit manually:[0m");console.log(` ${e}`)}}async function runWithRetry(e,t,o,s){try{await e(t)}catch(r){if(r instanceof M&&r.code==="token_expired"&&!s){console.log("\nToken expired, re-authenticating...");t.token=await n({api:o,force:true});await e(t)}else{throw r}}}function validateSpeed(e,t){if(t!==undefined){if(isNaN(t)||t<.5||t>2){console.error(`Error: --speed must be between 0.5 and 2.0 (got: "${parseFlag(e,"--speed")}")`);process.exit(1)}}}function validateSilence(e,t){if(t!==undefined){if(isNaN(t)||t<0||t>5){console.error(`Error: --silence must be between 0 and 5.0 (got: "${parseFlag(e,"--silence")}")`);process.exit(1)}}}function validateOutput(e,t){if(e){const t=[".wav",".mp3"];const o=t.some((t=>e.toLowerCase().endsWith(t)));if(!o){console.error("Error: --output path must end with .wav or .mp3");process.exit(1)}}}function validateFormat(e){if(e&&!["pcm","wav","mp3"].includes(e)){console.error(`Error: --format must be one of: pcm, wav, mp3 (got: "${e}")`);process.exit(1)}}function parseFlag(e,t){const o=e.indexOf(t);if(o===-1||o+1>=e.length)return null;return e[o+1]}function parseIntFlag(e,t){const o=parseFlag(e,t);return o!=null?parseInt(o,10):undefined}function parseFloatFlag(e,t){const o=parseFlag(e,t);return o!=null?parseFloat(o):undefined}function parseBoolFlag(e,t){return e.includes(t)}function printHelp(){console.log(`\nvoxflow v${I.version} — AI audio content creation CLI\n\nUsage:\n voxflow <command> [options]\n\nCommands:\n login Open browser to login and cache token\n logout Clear cached token\n status Show login status and token info\n dashboard Open Web dashboard in browser\n story [opts] Generate a story with TTS narration\n podcast [opts] Generate a multi-speaker podcast/dialogue\n synthesize <text> Synthesize a single text snippet to audio (alias: say)\n narrate [opts] Narrate a file, text, or script to audio\n voices [opts] Browse and search available TTS voices\n dub [opts] Dub video/audio from SRT subtitles (timeline-aligned TTS)\n asr [opts] Transcribe audio/video to text (alias: transcribe)\n translate [opts] Translate SRT subtitles, text, or files\n video-translate Translate entire video: ASR → translate → dub → merge\n\nStory options:\n --topic <text> Story topic (default: children's story)\n --voice <id> TTS voice ID (default: ${$.voice})\n --output <path> Output WAV path (default: ./story-<timestamp>.wav)\n --paragraphs <n> Paragraph count, 1-20 (default: ${$.paragraphs})\n --speed <n> TTS speed 0.5-2.0 (default: ${$.speed})\n --silence <sec> Silence between paragraphs, 0-5.0 (default: ${$.silence})\n\nPodcast options:\n --topic <text> Podcast topic (default: tech trends)\n --style <style> Dialogue style (default: ${y.style})\n --length <len> short | medium | long (default: ${y.length})\n --exchanges <n> Number of exchanges, 2-30 (default: ${y.exchanges})\n --output <path> Output WAV path (default: ./podcast-<timestamp>.wav)\n --speed <n> TTS speed 0.5-2.0 (default: ${y.speed})\n --silence <sec> Silence between segments, 0-5.0 (default: ${y.silence})\n\nSynthesize options (alias: say):\n <text> Text to synthesize (positional arg or --text)\n --text <text> Text to synthesize (alternative to positional)\n --voice <id> TTS voice ID (default: ${b.voice})\n --format <fmt> Output format: pcm, wav, mp3 (default: pcm → WAV)\n --speed <n> TTS speed 0.5-2.0 (default: ${b.speed})\n --volume <n> TTS volume 0.1-2.0 (default: ${b.volume})\n --pitch <n> TTS pitch -12 to 12 (default: ${b.pitch})\n --output <path> Output file path (default: ./tts-<timestamp>.wav)\n\nNarrate options:\n --input <file> Input .txt or .md file\n --text <text> Inline text to narrate\n --script <file> JSON script with per-segment voice/speed control\n --voice <id> Default voice ID (default: ${T.voice})\n --format <fmt> Output format: pcm, wav, mp3 (default: pcm → WAV)\n --speed <n> TTS speed 0.5-2.0 (default: ${T.speed})\n --silence <sec> Silence between segments, 0-5.0 (default: ${T.silence})\n --output <path> Output file path (default: ./narration-<timestamp>.wav)\n\n Also supports stdin: echo "text" | voxflow narrate\n\nDub options:\n --srt <file> SRT subtitle file (required)\n --video <file> Video file — merge dubbed audio into video\n --voice <id> Default TTS voice ID (default: ${k.voice})\n --voices <file> JSON speaker→voiceId map for multi-speaker dubbing\n --speed <n> TTS speed 0.5-2.0 (default: ${k.speed})\n --speed-auto Auto-adjust speed when audio overflows time slot\n --bgm <file> Background music file to mix in\n --ducking <n> BGM volume ducking 0-1.0 (default: ${k.ducking})\n --patch <id> Re-synthesize a single caption by ID (patch mode)\n --output <path> Output file path (default: ./dub-<timestamp>.wav)\n\nASR options (alias: transcribe):\n --input <file> Local audio or video file to transcribe\n --url <url> Remote audio URL to transcribe (cloud only)\n --mic Record from microphone (cloud only, requires sox)\n --engine <type> auto (default) | local | cloud\n --model <name> Whisper model: tiny, base (default), small, medium, large\n --mode <type> auto (default) | sentence | flash | file (cloud only)\n --lang <model> Language: 16k_zh (default), 16k_en, 16k_zh_en, 16k_ja, 16k_ko\n --format <fmt> Output format: srt (default), txt, json\n --output <path> Output file path (default: <input>.<format>)\n --speakers Enable speaker diarization (cloud flash/file mode)\n --speaker-number <n> Expected number of speakers (with --speakers)\n --task-id <id> Resume polling an existing async task (cloud only)\n\nTranslate options:\n --srt <file> SRT subtitle file to translate\n --text <text> Inline text to translate\n --input <file> Text file (.txt, .md) to translate\n --from <lang> Source language code (default: auto-detect)\n --to <lang> Target language code (required)\n --output <path> Output file path (default: <input>-<lang>.<ext>)\n --realign Adjust subtitle timing for target language length\n --batch-size <n> Captions per LLM call, 1-20 (default: ${E.batchSize})\n\n Supported languages: zh, en, ja, ko, fr, de, es, pt, ru, ar, th, vi, it\n\nVideo-translate options:\n --input <file> Input video file (required)\n --to <lang> Target language code (required)\n --from <lang> Source language code (default: auto-detect)\n --voice <id> TTS voice ID for dubbed audio\n --voices <file> JSON speaker→voiceId map for multi-speaker dubbing\n --realign Adjust subtitle timing for target language length\n --speed <n> TTS speed 0.5-2.0 (default: ${_.speed})\n --batch-size <n> Translation batch size, 1-20 (default: ${_.batchSize})\n --keep-intermediates Keep intermediate files (SRT, audio) for debugging\n --output <path> Output MP4 path (default: <input>-<lang>.mp4)\n --asr-mode <mode> Override ASR mode: auto, sentence, flash, file\n --asr-lang <engine> Override ASR engine: 16k_zh, 16k_en, 16k_ja, 16k_ko, etc.\n\nVoices options:\n --search <query> Search by name, tone, style, description\n --gender <m|f> Filter by gender: male/m or female/f\n --language <code> Filter by language: zh, en, etc.\n --extended Include extended voice library (380+ voices)\n --json Output raw JSON instead of table\n\nCommon options:\n --help, -h Show this help\n --version, -v Show version\n\nAdvanced options:\n --api <url> Override API endpoint (for self-hosted servers)\n --token <jwt> Use explicit token (CI/CD, skip browser login)\n\nExamples:\n voxflow say "你好世界"\n voxflow say "你好世界" --format mp3\n voxflow synthesize "Welcome" --voice v-male-Bk7vD3xP --format mp3\n voxflow narrate --input article.txt --voice v-female-R2s4N9qJ\n voxflow narrate --input article.txt --format mp3\n voxflow narrate --script narration-script.json\n echo "Hello" | voxflow narrate --output hello.wav\n voxflow voices --search "温柔" --gender female\n voxflow voices --extended --json\n voxflow dub --srt subtitles.srt\n voxflow dub --srt subtitles.srt --video input.mp4 --output dubbed.mp4\n voxflow dub --srt subtitles.srt --voices speakers.json --speed-auto\n voxflow dub --srt subtitles.srt --bgm music.mp3 --ducking 0.3\n voxflow dub --srt subtitles.srt --patch 5 --output dub-existing.wav\n voxflow asr --input recording.mp3\n voxflow asr --input recording.mp3 --engine local\n voxflow asr --input meeting.wav --engine local --model small\n voxflow asr --input video.mp4 --format srt --lang 16k_zh\n voxflow asr --url https://example.com/audio.wav --mode flash\n voxflow asr --mic --format txt\n voxflow transcribe --input meeting.wav --speakers --speaker-number 3\n voxflow asr --task-id 12345678 --format srt\n voxflow translate --srt subtitles.srt --to en\n voxflow translate --srt subtitles.srt --from zh --to en --realign\n voxflow translate --srt subtitles.srt --to ja --output subtitles-ja.srt\n voxflow translate --text "你好世界" --to en\n voxflow translate --input article.txt --to en --output article-en.txt\n voxflow video-translate --input video.mp4 --to en\n voxflow video-translate --input video.mp4 --from zh --to en --realign\n voxflow video-translate --input video.mp4 --to ja --voice v-male-Bk7vD3xP\n`)}e.exports={run:run}},929:(e,t,o)=>{const n=o(896);const s=o(928);const{API_BASE:r}=o(782);const{ApiError:a}=o(852);const{getMediaInfo:i,extractAudioForAsr:c}=o(388);const{uploadFileToCos:l}=o(567);const{recognize:u,detectMode:p,SENTENCE_MAX_MS:d,FLASH_MAX_MS:f,BASE64_MAX_BYTES:g,TASK_STATUS:m}=o(514);const{formatSrt:h,formatPlainText:w,formatJson:x,buildCaptionsFromFlash:v,buildCaptionsFromSentence:S,buildCaptionsFromFile:$}=o(813);const{checkWhisperAvailable:y,transcribeLocal:b}=o(126);const T={lang:"16k_zh",mode:"auto",format:"srt"};const k={"16k_zh":"中文 (16kHz)","16k_en":"English (16kHz)","16k_zh_en":"中英混合 (16kHz)","16k_ja":"日本語 (16kHz)","16k_ko":"한국어 (16kHz)","16k_zh_dialect":"中文方言 (16kHz)","8k_zh":"中文 (8kHz 电话)","8k_en":"English (8kHz phone)"};const F={srt:".srt",txt:".txt",json:".json"};async function asr(e){const sigintHandler=()=>{console.log("\n\nASR cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _asr(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _asr(e){const{token:t,api:a=r,input:d,url:f,mic:y=false,mode:b=T.mode,lang:E=T.lang,format:_=T.format,output:A,speakers:I=false,speakerNumber:M=0,taskId:L,engine:C="auto",model:N="base"}=e;if(L){return await resumePoll({apiBase:a,token:t,taskId:L,format:_,output:A,lang:E})}const P=resolveEngine(C);if(P==="local"){return await _asrLocal({input:d,format:_,output:A,model:N,lang:E})}const O=[d,f,y].filter(Boolean).length;if(O===0){throw new Error("No input specified. Provide one of:\n"+" --input <file> Local audio/video file\n"+" --url <url> Remote audio URL\n"+" --mic Record from microphone")}if(O>1){throw new Error("Specify only one input source: --input, --url, or --mic")}console.log("\n=== VoxFlow ASR ===");let D=d?s.resolve(d):null;let q=[];if(y){D=await handleMicInput();q.push(D)}if(D&&!n.existsSync(D)){throw new Error(`Input file not found: ${D}`)}let z=0;let R=0;let B=f||null;if(D){console.log(`Input: ${s.basename(D)}`);const e=await i(D);z=e.durationMs;R=n.statSync(D).size;const t=formatDuration(z);const o=formatSize(R);console.log(`Duration: ${t}`);console.log(`Size: ${o}`);if(!e.hasAudio){throw new Error("Input file has no audio track.")}console.log(`\n[1/3] 提取音频 (16kHz mono WAV)...`);const r=await c(D);q.push(r.wavPath);z=r.durationMs;R=n.statSync(r.wavPath).size;D=r.wavPath;console.log(` OK (${formatSize(R)}, ${formatDuration(z)})`)}else{console.log(`Input: ${f}`);console.log(`(Remote URL — duration will be detected by ASR API)`)}const U=!!B;const j=b==="auto"?p(z,U||!!D,R):b;console.log(`Mode: ${j}`);console.log(`Language: ${k[E]||E}`);console.log(`Format: ${_}`);if(D&&!B){const e=j==="flash"||j==="file"&&R>g||j==="sentence"&&R>g;if(e){console.log(`\n[2/3] 上传至 COS...`);const e=await l(D,a,t);B=e.cosUrl;console.log(` OK (${e.key})`)}else{console.log(`\n[2/3] 上传至 COS... (跳过,使用 base64)`)}}else if(!D&&B){console.log(`\n[2/3] 上传至 COS... (跳过,使用远程 URL)`)}console.log(`\n[3/3] ASR 语音识别 (${j})...`);const W=Date.now();const V=await u({apiBase:a,token:t,mode:j,url:B,filePath:j==="sentence"&&!B?D:undefined,durationMs:z,fileSize:R,lang:E,speakerDiarization:I,speakerNumber:M,wordInfo:_==="srt",onProgress:(e,t)=>{const o=e===m.WAITING?"排队中":e===m.PROCESSING?"识别中":"未知";process.stdout.write(`\r ${o}... (${Math.round(t/1e3)}s)`)}});const G=((Date.now()-W)/1e3).toFixed(1);console.log(`\n OK (${G}s)`);const K=V.audioTime||z/1e3||0;let J;switch(V.mode){case"flash":J=v(V.flashResult||[]);break;case"sentence":J=S(V.result,K,V.wordList);break;case"file":J=$(V.result,K);break;default:J=[{id:1,startMs:0,endMs:0,text:V.result||""}]}let H;switch(_){case"srt":H=h(J);break;case"txt":H=w(J,{includeSpeakers:I});break;case"json":H=x(J);break;default:throw new Error(`Unknown format: ${_}. Use: srt, txt, json`)}const Y=F[_]||".txt";let Q;if(A){Q=s.resolve(A)}else if(d){const e=s.basename(d,s.extname(d));Q=o.ab+"cli/"+s.dirname(d)+"/"+e+""+Y}else if(y){Q=s.resolve(`mic-${Date.now()}${Y}`)}else{try{const e=new URL(f);const t=s.basename(e.pathname,s.extname(e.pathname))||"asr";Q=o.ab+"cli/"+t+""+Y}catch{Q=s.resolve(`asr-${Date.now()}${Y}`)}}n.writeFileSync(Q,H,"utf8");for(const e of q){try{n.unlinkSync(e)}catch{}}const X=V.quota||{};const Z=1;const ee=X.remaining??"?";console.log(`\n=== Done ===`);console.log(`Output: ${Q}`);console.log(`Captions: ${J.length}`);console.log(`Duration: ${formatDuration(z||(V.audioTime||0)*1e3)}`);console.log(`Mode: ${V.mode}`);console.log(`Quota: ${Z} used, ${ee} remaining`);if(J.length>0&&_!=="json"){console.log(`\n--- Preview ---`);const e=J.slice(0,3);for(const t of e){const e=formatDuration(t.startMs);const o=t.speakerId?`[${t.speakerId}] `:"";const n=t.text.length>60?t.text.slice(0,57)+"...":t.text;console.log(` ${e} ${o}${n}`)}if(J.length>3){console.log(` ... (${J.length-3} more)`)}}return{outputPath:Q,mode:V.mode,duration:z/1e3,captionCount:J.length,quotaUsed:Z}}async function _asrLocal(e){const{input:t,format:r=T.format,output:a,model:l="base",lang:u=T.lang}=e;console.log("\n=== VoxFlow ASR (Local Whisper) ===");if(!t){throw new Error("Local whisper engine requires --input <file>.\n"+"URL and microphone input are cloud-only features.\n"+"Use: voxflow asr --input <file> --engine local")}const p=s.resolve(t);if(!n.existsSync(p)){throw new Error(`Input file not found: ${p}`)}const d=await i(p);console.log(`Input: ${s.basename(p)}`);console.log(`Duration: ${formatDuration(d.durationMs)}`);console.log(`Engine: whisper (local)`);console.log(`Model: ${l}`);console.log(`\n[1/2] 提取音频 (16kHz mono WAV)...`);const f=await c(p);console.log(` OK (${formatSize(n.statSync(f.wavPath).size)})`);console.log(`[2/2] Whisper 本地识别...`);const g=Date.now();const m=await b(f.wavPath,{model:l,lang:u});const v=((Date.now()-g)/1e3).toFixed(1);console.log(` OK (${v}s, ${m.length} segments)`);try{n.unlinkSync(f.wavPath)}catch{}let S;switch(r){case"srt":S=h(m);break;case"txt":S=w(m);break;case"json":S=x(m);break;default:throw new Error(`Unknown format: ${r}. Use: srt, txt, json`)}const $=F[r]||".txt";const y=a?s.resolve(a):o.ab+"cli/"+s.dirname(t)+"/"+s.basename(t,s.extname(t))+""+$;n.writeFileSync(y,S,"utf8");console.log(`\n=== Done ===`);console.log(`Output: ${y}`);console.log(`Captions: ${m.length}`);console.log(`Duration: ${formatDuration(d.durationMs)}`);console.log(`Engine: whisper (local, no quota used)`);if(m.length>0&&r!=="json"){console.log(`\n--- Preview ---`);const e=m.slice(0,3);for(const t of e){const e=formatDuration(t.startMs);const o=t.text.length>60?t.text.slice(0,57)+"...":t.text;console.log(` ${e} ${o}`)}if(m.length>3){console.log(` ... (${m.length-3} more)`)}}return{outputPath:y,mode:"local",duration:d.durationMs/1e3,captionCount:m.length,quotaUsed:0}}function resolveEngine(e){if(e==="local"||e==="whisper"){const e=y();if(!e.available){throw new Error("Local whisper engine requires nodejs-whisper.\n"+"Install: npm install -g nodejs-whisper\n"+"Download a model: npx nodejs-whisper download\n"+"Or use: --engine cloud")}return"local"}if(e==="cloud"||e==="tencent")return"cloud";if(e==="auto"){const{available:e}=y();return e?"local":"cloud"}return"cloud"}async function resumePoll({apiBase:e,token:t,taskId:r,format:a,output:i,lang:c}){console.log(`\n=== VoxFlow ASR — Resume Task ===`);console.log(`Task ID: ${r}`);const{pollTaskResult:l,TASK_STATUS:u}=o(514);console.log(`Polling...`);const p=await l({apiBase:e,token:t,taskId:r,onProgress:(e,t)=>{const o=e===u.WAITING?"排队中":e===u.PROCESSING?"识别中":"?";process.stdout.write(`\r ${o}... (${Math.round(t/1e3)}s)`)}});console.log(`\n OK`);const d=$(p.result,p.audioTime);let f;switch(a){case"srt":f=h(d);break;case"txt":f=w(d);break;case"json":f=x(d);break;default:f=w(d)}const g=F[a]||".txt";const m=i?s.resolve(i):s.resolve(`task-${r}${g}`);n.writeFileSync(m,f,"utf8");console.log(`\n=== Done ===`);console.log(`Output: ${m}`);console.log(`Captions: ${d.length}`);console.log(`Duration: ${formatDuration((p.audioTime||0)*1e3)}`);return{outputPath:m,mode:"file",duration:p.audioTime,captionCount:d.length}}async function handleMicInput(){const{recordMic:e,checkRecAvailable:t}=o(384);const n=await t();if(!n.available){throw new Error(n.error)}console.log(`\nRecording from microphone...`);console.log(` Press Enter or Q to stop recording.`);console.log(` Max duration: 5 minutes.\n`);const{wavPath:s,durationMs:r,stopped:a}=await e({maxSeconds:300});console.log(`\n Recording ${a==="user"?"stopped":"finished"}: ${formatDuration(r)}`);return s}function formatDuration(e){if(!e||e<=0)return"0s";const t=Math.round(e/1e3);if(t<60)return`${t}s`;const o=Math.floor(t/60);const n=t%60;if(o<60)return`${o}m${n>0?n+"s":""}`;const s=Math.floor(o/60);const r=o%60;return`${s}h${r>0?r+"m":""}`}function formatSize(e){if(e<1024)return`${e} B`;if(e<1024*1024)return`${(e/1024).toFixed(1)} KB`;return`${(e/1024/1024).toFixed(1)} MB`}e.exports={asr:asr,ASR_DEFAULTS:T,ApiError:a}},944:(e,t,o)=>{const n=o(896);const s=o(928);const{DUB_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{buildWav:u,getFileExtension:p}=o(56);const{parseSrt:d,formatSrt:f}=o(813);const{buildTimelinePcm:g,buildTimelineAudio:m,msToBytes:h,BYTES_PER_MS:w}=o(907);const{startSpinner:x}=o(339);function parseVoicesMap(e){if(!n.existsSync(e)){throw new Error(`Voices map file not found: ${e}`)}let t;try{t=JSON.parse(n.readFileSync(e,"utf8"))}catch(e){throw new Error(`Invalid JSON in voices map: ${e.message}`)}if(typeof t!=="object"||t===null||Array.isArray(t)){throw new Error('Voices map must be a JSON object: { "SpeakerName": "voiceId", ... }')}for(const[e,o]of Object.entries(t)){if(typeof o!=="string"||o.trim().length===0){throw new Error(`Invalid voice ID for speaker "${e}": must be a non-empty string`)}}return t}async function synthesizeCaption(e,t,o,n,s,r,l,u){process.stdout.write(` TTS [${l+1}/${u}]...`);const p={text:o,voiceId:n,speed:s??1,format:r||"pcm"};let d,f;try{({status:d,data:f}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},p))}catch(t){console.log(" FAIL");c(t,e)}if(d!==200||f.code!=="success"){console.log(" FAIL");i(d,f,`TTS caption ${l+1}`)}const g=Buffer.from(f.audio,"base64");const m=g.length/w;console.log(` OK (${(g.length/1024).toFixed(0)} KB, ${(m/1e3).toFixed(1)}s)`);return{audio:g,quota:f.quota,durationMs:m}}async function dub(e){const sigintHandler=()=>{console.log("\n\nDubbing cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _dub(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _dub(e){const t=e.srt;if(!t){throw new Error("No SRT file provided. Usage: voxflow dub --srt <file.srt>")}const a=s.resolve(t);if(!n.existsSync(a)){throw new Error(`SRT file not found: ${a}`)}const i=n.readFileSync(a,"utf8");const c=d(i);if(c.length===0){throw new Error("SRT file contains no valid captions")}const l=e.voice||r.voice;const u=e.speed??r.speed;const p=e.speedAuto||false;const f=r.toleranceMs;const g=e.api;const h=e.token;const w=e.patch;const x=[];let v=null;if(e.voicesMap){v=parseVoicesMap(s.resolve(e.voicesMap))}let S=e.output;const $=!!e.video;const y=$?".mp4":".wav";if(!S){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);S=s.resolve(`dub-${e}${y}`)}console.log("\n=== VoxFlow Dub ===");console.log(`SRT: ${t} (${c.length} captions)`);console.log(`Voice: ${l}${v?` + voices map (${Object.keys(v).length} speakers)`:""}`);console.log(`Speed: ${u}${p?" (auto-compensate)":""}`);if($)console.log(`Video: ${e.video}`);if(e.bgm)console.log(`BGM: ${e.bgm} (ducking: ${e.ducking??r.ducking})`);if(w!=null)console.log(`Patch: caption #${w}`);console.log(`Output: ${S}`);if(w!=null){return _dubPatch(e,c,S,x)}console.log(`\n[1/2] 合成 TTS 音频 (${c.length} 条字幕)...`);const b=[];let T=null;let k=0;for(let e=0;e<c.length;e++){const t=c[e];const o=t.endMs-t.startMs;let n=l;if(v&&t.speakerId&&v[t.speakerId]){n=v[t.speakerId]}let s=await synthesizeCaption(g,h,t.text,n,u,"pcm",e,c.length);k++;T=s.quota;if(p&&s.durationMs>o+f){const r=s.durationMs/o;if(r<=2){const a=Math.min(u*r,2);process.stdout.write(` ↳ Re-synth #${t.id} (${(s.durationMs/1e3).toFixed(1)}s > ${(o/1e3).toFixed(1)}s, speed: ${a.toFixed(2)})...`);s=await synthesizeCaption(g,h,t.text,n,a,"pcm",e,c.length);k++;T=s.quota}else{const a=`Caption #${t.id}: audio too long (${(s.durationMs/1e3).toFixed(1)}s for ${(o/1e3).toFixed(1)}s slot, alpha=${r.toFixed(1)}). Consider shortening text.`;x.push(a);console.log(` ⚠ OVERFLOW: ${a}`);const i=await synthesizeCaption(g,h,t.text,n,2,"pcm",e,c.length);k++;T=i.quota;s=i}}b.push({startMs:t.startMs,endMs:t.endMs,audioBuffer:s.audio})}console.log("\n[2/2] 构建时间轴音频...");const{wav:F,duration:E}=m(b);const _=$?S.replace(/\.[^.]+$/,".wav"):S;const A=s.dirname(_);n.mkdirSync(A,{recursive:true});n.writeFileSync(_,F);const I=_.replace(/\.(wav|mp3|mp4)$/i,".txt");const M=c.map((e=>{const t=e.speakerId?`|${e.speakerId}`:"";const o=v&&e.speakerId&&v[e.speakerId]?`|${v[e.speakerId]}`:"";return`[${e.id}${t}${o}] ${e.text}`})).join("\n\n");n.writeFileSync(I,M,"utf8");const L=$||e.bgm;if(L){const{checkFfmpeg:t,mergeAudioVideo:s,mixWithBgm:a}=o(297);const i=await t();if(!i.available){throw new Error("ffmpeg is required for BGM mixing / video merging. Install it:\n"+" macOS: brew install ffmpeg\n"+" Ubuntu: sudo apt install ffmpeg\n"+" Windows: https://ffmpeg.org/download.html")}let c=_;if(e.bgm){const t=_.replace(".wav","-mixed.wav");console.log(` Mixing BGM (ducking: ${e.ducking??r.ducking})...`);await a(_,e.bgm,t,{ducking:e.ducking??r.ducking});c=t;if(!$){n.copyFileSync(c,_);try{n.unlinkSync(c)}catch{}c=_}}if($){console.log(" Merging with video...");await s(e.video,c,S);try{if(_!==S)n.unlinkSync(_);if(e.bgm){const e=_.replace(".wav","-mixed.wav");if(n.existsSync(e))n.unlinkSync(e)}}catch{}}}console.log(`\n=== Done ===`);console.log(`Output: ${S} (${(n.statSync(S).size/1024).toFixed(1)} KB)`);console.log(`Duration: ${E.toFixed(1)}s`);console.log(`Transcript: ${I}`);console.log(`Captions: ${c.length}`);console.log(`Quota: ${k} used, ${T?.remaining??"?"} remaining`);if(x.length>0){console.log(`\nWarnings (${x.length}):`);for(const e of x){console.log(` ⚠ ${e}`)}}return{outputPath:S,textPath:I,duration:E,quotaUsed:k,segmentCount:c.length,warnings:x}}async function _dubPatch(e,t,o,a){const i=e.patch;const c=e.api;const l=e.token;const p=e.voice||r.voice;const d=e.speed??r.speed;let f=null;if(e.voicesMap){f=parseVoicesMap(s.resolve(e.voicesMap))}const g=t.findIndex((e=>e.id===i));if(g===-1){throw new Error(`Caption #${i} not found in SRT. Available IDs: ${t.map((e=>e.id)).join(", ")}`)}const m=t[g];const w=o.replace(/\.[^.]+$/,".wav");if(!n.existsSync(w)){throw new Error(`Patch mode requires an existing output file. `+`Run a full dub first, then use --patch to update individual captions.`)}let x=p;if(f&&m.speakerId&&f[m.speakerId]){x=f[m.speakerId]}console.log(`\n[Patch] Re-synthesizing caption #${i}: "${m.text.slice(0,40)}..."`);const v=await synthesizeCaption(c,l,m.text,x,d,"pcm",0,1);const S=n.readFileSync(w);const $=S.subarray(44);const y=h(m.startMs);const b=h(m.endMs);$.fill(0,y,Math.min(b,$.length));const T=Math.min(v.audio.length,b-y,$.length-y);if(T>0){v.audio.copy($,y,0,T)}const{wav:k}=u([$],0);n.writeFileSync(w,k);console.log(`\n=== Patch Done ===`);console.log(`Updated: caption #${i} in ${w}`);console.log(`Quota: 1 used, ${v.quota?.remaining??"?"} remaining`);return{outputPath:w,textPath:w.replace(/\.wav$/i,".txt"),duration:$.length/(24e3*2),quotaUsed:1,segmentCount:1,warnings:a}}e.exports={dub:dub,ApiError:l,_test:{parseVoicesMap:parseVoicesMap}}},80:(e,t,o)=>{const n=o(896);const s=o(928);const{NARRATE_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{parseParagraphs:u,buildWav:p,concatAudioBuffers:d,getFileExtension:f}=o(56);const{startSpinner:g}=o(339);function parseScript(e){if(!n.existsSync(e)){throw new Error(`Script file not found: ${e}`)}let t;try{t=JSON.parse(n.readFileSync(e,"utf8"))}catch(e){throw new Error(`Invalid JSON in script file: ${e.message}`)}if(!t.segments||!Array.isArray(t.segments)||t.segments.length===0){throw new Error('Script must have a non-empty "segments" array')}for(let e=0;e<t.segments.length;e++){const o=t.segments[e];if(!o.text||typeof o.text!=="string"||o.text.trim().length===0){throw new Error(`Segment ${e+1} must have a non-empty "text" field`)}}return{segments:t.segments.map((e=>({text:e.text.trim(),voiceId:e.voiceId||undefined,speed:e.speed!=null?Number(e.speed):undefined,volume:e.volume!=null?Number(e.volume):undefined,pitch:e.pitch!=null?Number(e.pitch):undefined}))),silence:t.silence!=null?Number(t.silence):r.silence,output:t.output||undefined}}function stripMarkdown(e){return e.replace(/```[\s\S]*?```/g,"").replace(/`([^`]+)`/g,"$1").replace(/!\[[^\]]*\]\([^)]*\)/g,"").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/^#{1,6}\s+/gm,"").replace(/\*{1,3}([^*]+)\*{1,3}/g,"$1").replace(/_{1,3}([^_]+)_{1,3}/g,"$1").replace(/^[-*_]{3,}\s*$/gm,"").replace(/^>\s?/gm,"").replace(/\n{3,}/g,"\n\n").trim()}async function readStdin(){const e=[];for await(const t of process.stdin){e.push(t)}return Buffer.concat(e).toString("utf8")}async function synthesizeSegment(e,t,o,n,s,r,l,u,p,d){process.stdout.write(` TTS [${p+1}/${d}]...`);const f={text:o,voiceId:n,speed:s??1,volume:r??1,format:u||"pcm"};if(l!=null&&l!==0)f.pitch=l;let g,m;try{({status:g,data:m}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},f))}catch(t){console.log(" FAIL");c(t,e)}if(g!==200||m.code!=="success"){console.log(" FAIL");i(g,m,`TTS segment ${p+1}`)}const h=Buffer.from(m.audio,"base64");const w=u==="mp3"?"MP3":u==="wav"?"WAV":"PCM";console.log(` OK (${(h.length/1024).toFixed(0)} KB ${w})`);return{audio:h,quota:m.quota}}async function narrate(e){const sigintHandler=()=>{console.log("\n\nNarration cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _narrate(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _narrate(e){const t=e.voice||r.voice;const o=e.speed??r.speed;const a=e.format||"pcm";const i=e.api;const c=e.token;let l;let g;let m;let h;if(e.script){const t=parseScript(e.script);l=t.segments;g=e.silence??t.silence;m=e.output||t.output;h=`script: ${e.script} (${l.length} segments)`}else if(e.input){const t=s.resolve(e.input);if(!n.existsSync(t)){throw new Error(`Input file not found: ${t}`)}let o=n.readFileSync(t,"utf8");const a=s.extname(t).toLowerCase();if(a===".md"||a===".markdown"){o=stripMarkdown(o)}const i=u(o);if(i.length===0){throw new Error("No text content found in input file")}l=i.map((e=>({text:e})));g=e.silence??r.silence;m=e.output;h=`file: ${e.input} (${l.length} paragraphs)`}else if(e.text){const t=u(e.text);if(t.length===0){throw new Error("No text content provided")}l=t.map((e=>({text:e})));g=e.silence??r.silence;m=e.output;h=`text: ${e.text.length} chars (${l.length} paragraphs)`}else if(!process.stdin.isTTY){const t=await readStdin();if(!t||t.trim().length===0){throw new Error("No input provided via stdin")}const o=u(t);if(o.length===0){throw new Error("No text content found in stdin input")}l=o.map((e=>({text:e})));g=e.silence??r.silence;h=`stdin (${l.length} paragraphs)`}else{throw new Error("No input provided. Use one of:\n"+" --input <file.txt> Read a text or markdown file\n"+' --text "text" Provide inline text\n'+" --script <file.json> Use a script with per-segment control\n"+' echo "text" | voxflow narrate Pipe from stdin')}const w=f(a);if(!m){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);m=s.resolve(`narration-${e}${w}`)}if(!m.endsWith(w)){m=m.replace(/\.(wav|mp3|pcm)$/i,"")+w}console.log("\n=== VoxFlow Narrate ===");console.log(`Input: ${h}`);console.log(`Voice: ${t}${e.script?" (may be overridden per segment)":""}`);console.log(`Format: ${a==="pcm"?"wav (pcm)":a}`);console.log(`Speed: ${o}`);if(a==="mp3"){console.log(`Output: ${m}`);console.log(` (MP3 模式不插入段间静音)`)}else{console.log(`Silence: ${g}s`);console.log(`Output: ${m}`)}console.log(`\n[1/2] 合成 TTS 音频 (${l.length} 段)...`);const x=[];let v=null;for(let e=0;e<l.length;e++){const n=l[e];const s=await synthesizeSegment(i,c,n.text,n.voiceId||t,n.speed??o,n.volume,n.pitch,a,e,l.length);x.push(s.audio);v=s.quota}console.log("\n[2/2] 拼接音频...");const{audio:S,wav:$,duration:y}=a==="mp3"||a==="wav"?d(x,a,g):p(x,g);const b=S||$;const T=s.dirname(m);n.mkdirSync(T,{recursive:true});n.writeFileSync(m,b);const k=m.replace(/\.(wav|mp3)$/i,".txt");const F=l.map(((e,t)=>{const o=e.voiceId?`[${t+1}|${e.voiceId}]`:`[${t+1}]`;return`${o} ${e.text}`})).join("\n\n");n.writeFileSync(k,F,"utf8");const E=l.length;console.log(`\n=== Done ===`);console.log(`Output: ${m} (${(b.length/1024).toFixed(1)} KB, ${y.toFixed(1)}s)`);console.log(`Transcript: ${k}`);console.log(`Segments: ${l.length}`);console.log(`Quota: ${E} used, ${v?.remaining??"?"} remaining`);return{outputPath:m,textPath:k,duration:y,quotaUsed:E,segmentCount:l.length,format:a}}e.exports={narrate:narrate,ApiError:l,_test:{parseScript:parseScript,stripMarkdown:stripMarkdown}}},35:(e,t,o)=>{const n=o(896);const s=o(928);const{PODCAST_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{buildWav:u}=o(56);const{startSpinner:p}=o(339);function parseDialogueText(e){const t=e.split("\n").filter((e=>e.trim()));const o=[];const n=/^([^::]+)[::]\s*(.+)$/;for(const e of t){const t=e.trim();if(!t)continue;const s=t.match(n);if(s){const e=s[1].trim();const t=s[2].trim();if(t){o.push({speaker:e,text:t})}}else if(t.length>0){o.push({speaker:"旁白",text:t})}}return o}async function generateDialogue(e,t,o){const n=p("\n[1/3] 生成对话文本...");let s,r;try{({status:s,data:r}=await a(`${e}/api/llm/generate-dialogue`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{prompt:o.topic,style:o.style,length:o.length,dialogueMode:true,autoSpeakerNames:true,exchanges:o.exchanges}))}catch(t){n.stop("FAIL");c(t,e)}if(s!==200||r.code!=="success"){n.stop("FAIL");i(s,r,"Dialogue generation")}const l=r.text;const u=r.voiceMapping||{};const d=r.quota;n.stop("OK");const f=parseDialogueText(l);const g=[...new Set(f.map((e=>e.speaker)))];console.log(` ${l.length} 字, ${f.length} 段, ${g.length} 位说话者`);console.log(` 说话者: ${g.join(", ")}`);console.log(` 配额剩余: ${d?.remaining??"?"}`);return{text:l,segments:f,voiceMapping:u,speakers:g,quota:d}}async function synthesizeSegment(e,t,o,n,s,r,l,u){process.stdout.write(` TTS [${r+1}/${l}] ${u}...`);let p,d;try{({status:p,data:d}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{text:o,voiceId:n,speed:s,volume:1}))}catch(t){console.log(" FAIL");c(t,e)}if(p!==200||d.code!=="success"){console.log(" FAIL");i(p,d,`TTS segment ${r+1}`)}const f=Buffer.from(d.audio,"base64");console.log(` OK (${(f.length/1024).toFixed(0)} KB)`);return{pcm:f,quota:d.quota}}async function synthesizeAll(e,t,o,n,s){console.log(`\n[2/3] 合成 TTS 音频 (${o.length} 段, 多角色)...`);const a=[];let i=null;for(let c=0;c<o.length;c++){const l=o[c];const u=n[l.speaker]||{};const p=u.voiceId||r.defaultVoice||"v-female-R2s4N9qJ";const d=u.speed||s;const f=await synthesizeSegment(e,t,l.text,p,d,c,o.length,l.speaker);a.push(f.pcm);i=f.quota}return{pcmBuffers:a,quota:i}}async function podcast(e){const sigintHandler=()=>{console.log("\n\nGeneration cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _podcast(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _podcast(e){const t=e.style||r.style;const o=e.length||r.length;const a=e.exchanges||r.exchanges;const i=e.speed??r.speed;const c=e.silence??r.silence;const l=e.api;const p=e.token;const d=e.topic||"科技领域的最新趋势";let f=e.output;if(!f){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);f=s.resolve(`podcast-${e}.wav`)}console.log("\n=== VoxFlow Podcast Generator ===");console.log(`Topic: ${d}`);console.log(`Style: ${t}`);console.log(`Length: ${o}`);console.log(`Exchanges: ${a}`);console.log(`Speed: ${i}`);console.log(`API: ${l}`);console.log(`Output: ${f}`);const{text:g,segments:m,voiceMapping:h,speakers:w}=await generateDialogue(l,p,{topic:d,style:t,length:o,exchanges:a});if(m.length===0){throw new Error("No dialogue segments found in generated text")}console.log("\n Voice assignments:");for(const e of w){const t=h[e];if(t){console.log(` ${e} → ${t.voiceId}`)}else{console.log(` ${e} → (default)`)}}const{pcmBuffers:x,quota:v}=await synthesizeAll(l,p,m,h,i);console.log("\n[3/3] 拼接音频...");const{wav:S,duration:$}=u(x,c);const y=s.dirname(f);n.mkdirSync(y,{recursive:true});n.writeFileSync(f,S);const b=f.replace(/\.wav$/,".txt");const T=m.map(((e,t)=>`[${t+1}] ${e.speaker}:${e.text}`)).join("\n\n");n.writeFileSync(b,T,"utf8");const k=2+m.length;console.log(`\n=== Done ===`);console.log(`Output: ${f} (${(S.length/1024).toFixed(1)} KB, ${$.toFixed(1)}s)`);console.log(`Script: ${b}`);console.log(`Quota: ${k} used, ${v?.remaining??"?"} remaining`);return{outputPath:f,textPath:b,duration:$,quotaUsed:k}}e.exports={podcast:podcast,ApiError:l,_test:{parseDialogueText:parseDialogueText}}},214:(e,t,o)=>{const n=o(896);const s=o(928);const{STORY_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{parseParagraphs:u,buildWav:p,createSilence:d}=o(56);const{startSpinner:f}=o(339);async function generateStory(e,t,o){const n=f("\n[1/3] 生成故事文本...");let s,r;try{({status:s,data:r}=await a(`${e}/api/llm/chat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{messages:[{role:"user",content:o}],model:"gpt-4o-mini",stream:false,temperature:.8}))}catch(t){n.stop("FAIL");c(t,e)}if(s!==200||r.code!=="success"){n.stop("FAIL");i(s,r,"LLM")}const l=r.content;const u=r.quota;n.stop("OK");console.log(` ${l.length} 字. 配额剩余: ${u?.remaining??"?"}`);return{story:l,quota:u}}async function synthesizeParagraph(e,t,o,n,s,r,l){process.stdout.write(` TTS [${r+1}/${l}]...`);let u,p;try{({status:u,data:p}=await a(`${e}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{text:o,voiceId:n,speed:s,volume:1}))}catch(t){console.log(" FAIL");c(t,e)}if(u!==200||p.code!=="success"){console.log(" FAIL");i(u,p,`TTS paragraph ${r+1}`)}const d=Buffer.from(p.audio,"base64");console.log(` OK (${(d.length/1024).toFixed(0)} KB)`);return{pcm:d,quota:p.quota}}async function synthesizeAll(e,t,o,n,s){console.log(`\n[2/3] 合成 TTS 音频 (${o.length} 段)...`);const r=[];let a=null;for(let i=0;i<o.length;i++){const c=await synthesizeParagraph(e,t,o[i],n,s,i,o.length);r.push(c.pcm);a=c.quota}return{pcmBuffers:r,quota:a}}async function story(e){const sigintHandler=()=>{console.log("\n\nGeneration cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _story(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _story(e){const t=e.voice||r.voice;const o=e.paragraphs||r.paragraphs;const a=e.speed??r.speed;const i=e.silence??r.silence;const c=e.api;const l=e.token;const d=e.topic||`请写一个适合5岁儿童的短故事,要求:\n1. 分${o}段,每段2-3句话\n2. 每段描述一个清晰的画面场景\n3. 语言简单易懂,充满童趣\n4. 段落之间用空行分隔\n5. 不要添加段落编号,直接输出故事内容`;let f=e.output;if(!f){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);f=s.resolve(`story-${e}.wav`)}console.log("\n=== VoxFlow Story Generator ===");console.log(`Voice: ${t}`);console.log(`API: ${c}`);console.log(`Paragraphs: ${o}`);console.log(`Speed: ${a}`);console.log(`Output: ${f}`);const{story:g}=await generateStory(c,l,d);const m=u(g);if(m.length===0){throw new Error("No paragraphs found in generated story")}console.log(` ${m.length} 段`);const{pcmBuffers:h,quota:w}=await synthesizeAll(c,l,m,t,a);console.log("\n[3/3] 拼接音频...");const{wav:x,duration:v}=p(h,i);const S=s.dirname(f);n.mkdirSync(S,{recursive:true});n.writeFileSync(f,x);const $=f.replace(/\.wav$/,".txt");const y=m.map(((e,t)=>`[${t+1}] ${e}`)).join("\n\n");n.writeFileSync($,y,"utf8");const b=1+m.length;console.log(`\n=== Done ===`);console.log(`Output: ${f} (${(x.length/1024).toFixed(1)} KB, ${v.toFixed(1)}s)`);console.log(`Story: ${$}`);console.log(`Quota: ${b} used, ${w?.remaining??"?"} remaining`);return{outputPath:f,textPath:$,duration:v,quotaUsed:b}}e.exports={story:story,ApiError:l,_test:{parseParagraphs:u,buildWav:p,createSilence:d}}},383:(e,t,o)=>{const n=o(896);const s=o(928);const{SYNTHESIZE_DEFAULTS:r}=o(782);const{request:a,throwApiError:i,throwNetworkError:c,ApiError:l}=o(852);const{buildWav:u,getFileExtension:p}=o(56);const{startSpinner:d}=o(339);async function synthesize(e){const sigintHandler=()=>{console.log("\n\nSynthesis cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _synthesize(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _synthesize(e){const t=e.text;if(!t||t.trim().length===0){throw new Error('No text provided. Usage: voxflow synthesize "your text here"')}const o=e.voice||r.voice;const l=e.speed??r.speed;const f=e.volume??r.volume;const g=e.pitch??r.pitch;const m=e.format||"pcm";const h=e.api;const w=e.token;const x=p(m);let v=e.output;if(!v){const e=(new Date).toISOString().replace(/[:.]/g,"-").slice(0,19);v=s.resolve(`tts-${e}${x}`)}console.log("\n=== VoxFlow Synthesize ===");console.log(`Voice: ${o}`);console.log(`Format: ${m==="pcm"?"wav (pcm)":m}`);console.log(`Speed: ${l}`);if(f!==1)console.log(`Volume: ${f}`);if(g!==0)console.log(`Pitch: ${g}`);console.log(`Text: ${t.length>60?t.slice(0,57)+"...":t}`);console.log(`Output: ${v}`);const S=d("\n[1/1] 合成 TTS 音频...");let $,y;try{({status:$,data:y}=await a(`${h}/api/tts/synthesize`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${w}`}},{text:t.trim(),voiceId:o,format:m,speed:l,volume:f,pitch:g}))}catch(e){S.stop("FAIL");c(e,h)}if($!==200||y.code!=="success"){S.stop("FAIL");i($,y,"TTS")}const b=Buffer.from(y.audio,"base64");S.stop("OK");let T,k;if(m==="mp3"){T=b;k=b.length/4e3;console.log(` ${(b.length/1024).toFixed(0)} KB MP3`)}else if(m==="wav"){T=b;const e=b.length>44?b.readUInt32LE(28):48e3;const t=b.length>44?b.readUInt32LE(40):b.length;k=t/e;console.log(` ${(b.length/1024).toFixed(0)} KB WAV`)}else{const e=u([b],0);T=e.wav;k=e.duration;console.log(` ${(b.length/1024).toFixed(0)} KB PCM → WAV`)}const F=s.dirname(v);n.mkdirSync(F,{recursive:true});n.writeFileSync(v,T);const E=1;console.log(`\n=== Done ===`);console.log(`Output: ${v} (${(T.length/1024).toFixed(1)} KB, ${k.toFixed(1)}s)`);console.log(`Quota: ${E} used, ${y.quota?.remaining??"?"} remaining`);return{outputPath:v,duration:k,quotaUsed:E,format:m}}e.exports={synthesize:synthesize,ApiError:l}},585:(e,t,o)=>{const n=o(896);const s=o(928);const{API_BASE:r,TRANSLATE_DEFAULTS:a}=o(782);const{chatCompletion:i,detectLanguage:c}=o(133);const{parseSrt:l,formatSrt:u}=o(813);const p={zh:"Chinese (Simplified)",en:"English",ja:"Japanese",ko:"Korean",fr:"French",de:"German",es:"Spanish",pt:"Portuguese",ru:"Russian",ar:"Arabic",th:"Thai",vi:"Vietnamese",it:"Italian"};function batchCaptions(e,t=10){const o=[];for(let n=0;n<e.length;n+=t){o.push(e.slice(n,n+t))}return o}function buildTranslationPrompt(e,t,o){const n=[`You are a professional subtitle translator. Translate each numbered line from ${t} to ${o}.`,"","Rules:","- Return ONLY the translated lines, one per number","- Keep the exact same numbering (1., 2., 3., ...)","- Preserve [Speaker: xxx] tags unchanged — do NOT translate speaker names","- Keep translations concise and natural for subtitles","- Do not add explanations, notes, or extra text"].join("\n");const s=e.map(((e,t)=>{const o=e.speakerId?`[Speaker: ${e.speakerId}] `:"";return`${t+1}. ${o}${e.text}`})).join("\n");return{system:n,user:s}}function parseTranslationResponse(e,t){const o=e.trim().split("\n").filter((e=>e.trim()));const n=[];for(let e=0;e<t.length;e++){const s=new RegExp(`^${e+1}\\.\\s*(.+)$`);const r=o.find((e=>s.test(e.trim())));if(r){const o=r.trim().replace(s,"$1").trim();let a=o;const i=o.match(/^\[Speaker:\s*[^\]]+\]\s*/i);if(i){a=o.slice(i[0].length)}n.push({...t[e],text:a||t[e].text})}else{if(e<o.length){const s=o[e].replace(/^\d+\.\s*/,"").trim();let r=s;const a=s.match(/^\[Speaker:\s*[^\]]+\]\s*/i);if(a){r=s.slice(a[0].length)}n.push({...t[e],text:r||t[e].text})}else{n.push({...t[e]})}}}return n}function realignTimings(e,t){const o=.3;const n=100;const s=t.map(((t,s)=>{const r=e[s];if(!r)return t;const a=r.text.length;const i=t.text.length;if(a===0)return t;const c=i/a;if(c<1+o&&c>1-o){return t}const l=r.endMs-r.startMs;let u=Math.round(l*c);const p=s<e.length-1?e[s+1].startMs:Infinity;const d=p-t.startMs-n;if(u>d&&d>0){u=d}u=Math.max(u,500);return{...t,endMs:t.startMs+u}}));return s}async function translate(e){const sigintHandler=()=>{console.log("\n\nTranslation cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _translate(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _translate(e){const{token:t,api:o=r,srt:n,text:s,input:i,from:c,to:l,output:u,realign:p=false,batchSize:d=a.batchSize}=e;if(n)return _translateSrt({token:t,api:o,srt:n,from:c,to:l,output:u,realign:p,batchSize:d});if(s)return _translateText({token:t,api:o,text:s,from:c,to:l});if(i)return _translateFile({token:t,api:o,input:i,from:c,to:l,output:u});throw new Error("No input specified. Use --srt, --text, or --input")}async function _translateSrt({token:e,api:t,srt:r,from:c,to:d,output:f,realign:g,batchSize:m}){console.log("\n=== VoxFlow Translate (SRT) ===");const h=s.resolve(r);const w=n.readFileSync(h,"utf8");const x=l(w);if(x.length===0){throw new Error(`SRT file is empty or invalid: ${h}`)}console.log(`Input: ${s.basename(h)}`);console.log(`Captions: ${x.length}`);const v=c||await autoDetectLanguage(t,x);const S=p[v]||v;const $=p[d]||d;console.log(`From: ${S} (${v})`);console.log(`To: ${$} (${d})`);console.log(`Realign: ${g?"yes":"no"}`);const y=batchCaptions(x,m);console.log(`Batches: ${y.length} (batch size: ${m})`);console.log("");let b=[];let T=0;for(let o=0;o<y.length;o++){const n=y[o];process.stdout.write(` [${o+1}/${y.length}] Translating ${n.length} captions...`);const{system:s,user:r}=buildTranslationPrompt(n,S,$);const c=await i({apiBase:t,token:e,messages:[{role:"system",content:s},{role:"user",content:r}],temperature:a.temperature,maxTokens:a.maxTokens});const l=parseTranslationResponse(c.content,n);b=b.concat(l);T++;if(c.quota){console.log(` OK (remaining: ${c.quota.remaining})`)}else{console.log(" OK")}}if(g){console.log(" Re-aligning subtitle timing...");b=realignTimings(x,b)}b=b.map(((e,t)=>({...e,id:t+1})));const k=u(b);let F;if(f){F=s.resolve(f)}else{const e=s.basename(h,s.extname(h));const t=s.dirname(h);F=o.ab+"cli/"+t+"/"+e+"-"+d+".srt"}n.writeFileSync(F,k,"utf8");console.log(`\n=== Done ===`);console.log(`Output: ${F}`);console.log(`Captions: ${b.length}`);console.log(`Quota: ${T} used`);if(b.length>0){console.log(`\n--- Preview ---`);const e=b.slice(0,3);for(const t of e){const e=t.speakerId?`[${t.speakerId}] `:"";const o=t.text.length>60?t.text.slice(0,57)+"...":t.text;console.log(` ${t.id}. ${e}${o}`)}if(b.length>3){console.log(` ... (${b.length-3} more)`)}}return{outputPath:F,captionCount:b.length,quotaUsed:T,from:v,to:d}}async function _translateText({token:e,api:t,text:o,from:n,to:s}){console.log("\n=== VoxFlow Translate (Text) ===");const r=n||await autoDetectLanguage(t,[{text:o}]);const c=p[r]||r;const l=p[s]||s;console.log(`From: ${c} → To: ${l}`);const u=await i({apiBase:t,token:e,messages:[{role:"system",content:`You are a professional translator. Translate the following text from ${c} to ${l}. Return ONLY the translation, no explanations.`},{role:"user",content:o}],temperature:a.temperature,maxTokens:a.maxTokens});const d=u.content.trim();console.log(`\n${d}`);const f=u.quota?u.quota.remaining:"?";console.log(`\n(Quota: 1 used, ${f} remaining)`);return{text:d,quotaUsed:1,from:r,to:s}}async function _translateFile({token:e,api:t,input:r,from:c,to:l,output:u}){console.log("\n=== VoxFlow Translate (File) ===");const d=s.resolve(r);const f=n.readFileSync(d,"utf8");if(f.trim().length===0){throw new Error(`Input file is empty: ${d}`)}console.log(`Input: ${s.basename(d)}`);console.log(`Length: ${f.length} chars`);const g=c||await autoDetectLanguage(t,[{text:f}]);const m=p[g]||g;const h=p[l]||l;console.log(`From: ${m} → To: ${h}`);const w=await i({apiBase:t,token:e,messages:[{role:"system",content:`You are a professional translator. Translate the following document from ${m} to ${h}. Preserve the original formatting (paragraphs, line breaks, markdown). Return ONLY the translation.`},{role:"user",content:f}],temperature:a.temperature,maxTokens:Math.max(a.maxTokens,4e3)});const x=w.content.trim();let v;if(u){v=s.resolve(u)}else{const e=s.extname(d);const t=s.basename(d,e);const n=s.dirname(d);v=o.ab+"cli/"+n+"/"+t+"-"+l+""+e}n.writeFileSync(v,x+"\n","utf8");const S=w.quota?w.quota.remaining:"?";console.log(`\n=== Done ===`);console.log(`Output: ${v}`);console.log(`Quota: 1 used, ${S} remaining`);return{outputPath:v,quotaUsed:1,from:g,to:l}}async function autoDetectLanguage(e,t){const o=t.slice(0,3).map((e=>e.text)).join(" ");const n=await c({apiBase:e,text:o});return n||"auto"}e.exports={translate:translate,LANG_MAP:p,_test:{buildTranslationPrompt:buildTranslationPrompt,parseTranslationResponse:parseTranslationResponse,realignTimings:realignTimings,batchCaptions:batchCaptions}}},863:(e,t,o)=>{const n=o(896);const s=o(928);const r=o(857);const{checkFfmpeg:a,extractAudio:i}=o(297);const{asr:c}=o(929);const{translate:l}=o(585);const{dub:u}=o(944);const{detectLanguage:p}=o(133);const{parseSrt:d}=o(813);const{API_BASE:f,VIDEO_TRANSLATE_DEFAULTS:g}=o(782);const m={zh:"16k_zh",en:"16k_en",ja:"16k_ja",ko:"16k_ko","zh-en":"16k_zh_en"};function resolveAsrLang(e,t){if(t)return t;if(e&&m[e])return m[e];return"16k_zh"}async function videoTranslate(e){const sigintHandler=()=>{console.log("\n\nVideo translation cancelled.");process.exit(0)};process.on("SIGINT",sigintHandler);try{return await _videoTranslate(e)}finally{process.removeListener("SIGINT",sigintHandler)}}async function _videoTranslate(e){const{token:t,api:m=f,input:h,from:w,to:x,voice:v,voicesMap:S,realign:$=false,output:y,keepIntermediates:b=false,batchSize:T=g.batchSize,speed:k=g.speed,asrMode:F,asrLang:E}=e;const _=s.resolve(h);const A=s.basename(_,s.extname(_));console.log("\n=== VoxFlow Video Translate ===");console.log(`Input: ${s.basename(_)}`);console.log(`Target: ${x}`);console.log("");const I=n.mkdtempSync(s.join(r.tmpdir(),"voxflow-vtranslate-"));let M=0;const L={};try{process.stdout.write("[1/4] Checking FFmpeg... ");const e=await a();if(!e.available){throw new Error("FFmpeg is required for video-translate. Install: https://ffmpeg.org/download.html")}console.log(`OK (${e.version})`);process.stdout.write("[2/4] Transcribing audio... ");const r=s.join(I,"extracted-audio.wav");await i(_,r);const f=s.join(I,"source.srt");const g=resolveAsrLang(w,E);const h={token:t,api:m,input:r,format:"srt",output:f,lang:g};if(F)h.mode=F;const C=await c(h);if(C.captionCount===0){throw new Error("ASR produced no captions. The video may have no audible speech.")}L.asr={mode:C.mode,duration:C.duration,captionCount:C.captionCount,quotaUsed:C.quotaUsed};M+=C.quotaUsed;console.log(`${C.captionCount} captions (${C.mode} mode)`);let N=w;if(!N){const e=n.readFileSync(f,"utf8");const t=d(e);const o=t.slice(0,3).map((e=>e.text)).join(" ");N=await p({apiBase:m,text:o})||"auto"}process.stdout.write(`[3/4] Translating (${N} → ${x})... `);const P=s.join(I,`translated-${x}.srt`);const O=await l({token:t,api:m,srt:f,from:N,to:x,output:P,realign:$,batchSize:T});L.translate={from:O.from,to:O.to,captionCount:O.captionCount,quotaUsed:O.quotaUsed};M+=O.quotaUsed;console.log(`${O.captionCount} captions translated`);process.stdout.write("[4/4] Dubbing and merging video... ");const D=y?s.resolve(y):o.ab+"cli/"+s.dirname(_)+"/"+A+"-"+x+".mp4";const q=await u({token:t,api:m,srt:P,voice:v,voicesMap:S,speed:k,video:_,output:D});L.dub={segmentCount:q.segmentCount,duration:q.duration,quotaUsed:q.quotaUsed,warnings:q.warnings};M+=q.quotaUsed;console.log(`${q.segmentCount} segments dubbed`);if(b){const e=s.resolve(s.dirname(D),`${A}-${x}-intermediates`);n.mkdirSync(e,{recursive:true});const t=[["extracted-audio.wav",r],["source.srt",f],[`translated-${x}.srt`,P]];for(const[o,r]of t){if(n.existsSync(r)){n.copyFileSync(r,s.join(e,o))}}console.log(`\nIntermediates saved: ${e}`)}console.log("\n=== Done ===");console.log(`Output: ${D}`);console.log(`Language: ${N} → ${x}`);console.log(`Captions: ${O.captionCount}`);console.log(`Duration: ${q.duration.toFixed(1)}s`);console.log(`Quota: ${M} used`);if(L.dub.warnings&&L.dub.warnings.length>0){console.log(`\nWarnings:`);for(const e of L.dub.warnings){console.log(` - ${e}`)}}return{outputPath:D,from:N,to:x,captionCount:O.captionCount,quotaUsed:M,stages:L}}finally{if(!b){try{n.rmSync(I,{recursive:true,force:true})}catch{}}}}e.exports={videoTranslate:videoTranslate}},784:(e,t,o)=>{const{request:n,throwNetworkError:s}=o(852);async function voices(e){const t=e.api;const o=e.extended?"true":"false";let r,a;try{({status:r,data:a}=await n(`${t}/api/tts/voices?includeExtended=${o}`,{method:"GET"}))}catch(e){s(e,t)}if(r!==200){throw new Error(`Failed to fetch voices (${r}): ${a?.message||"unknown error"}`)}let i=a.voices||a.data?.voices||[];if(e.gender){const t=normalizeGender(e.gender);if(!t){console.error(`Error: --gender must be one of: male, m, female, f (got: "${e.gender}")`);process.exit(1)}i=i.filter((e=>{const o=(e.gender||"").toLowerCase();return o===t}))}if(e.language){const t=e.language.toLowerCase();i=i.filter((e=>(e.language||"").toLowerCase()===t))}if(e.search){const t=e.search.toLowerCase();i=i.filter((e=>{const o=[e.name,e.nameEn,e.tone,e.style,e.description,e.scenarios].filter(Boolean).join(" ").toLowerCase();return o.includes(t)}))}if(i.length===0){console.log("No voices match your criteria.");return}if(e.json){console.log(JSON.stringify(i,null,2))}else{printTable(i)}console.log(`\nFound ${i.length} voice${i.length===1?"":"s"}.`)}function normalizeGender(e){const t=(e||"").toLowerCase().trim();if(t==="male"||t==="m")return"male";if(t==="female"||t==="f")return"female";return null}function printTable(e){const t=24;const o=14;const n=8;const s=22;const r=20;const a=["ID".padEnd(t),"Name".padEnd(o),"Gender".padEnd(n),"Tone".padEnd(s),"Style".padEnd(r)].join(" ");console.log(`\n${a}`);console.log("-".repeat(a.length));for(const a of e){const e=[truncate(a.id||"",t).padEnd(t),truncate(a.name||"",o).padEnd(o),truncate(a.gender||"",n).padEnd(n),truncate(a.tone||"",s).padEnd(s),truncate(a.style||"",r).padEnd(r)].join(" ");console.log(e)}}function truncate(e,t){if(e.length<=t)return e;return e.slice(0,t-1)+"…"}e.exports={voices:voices}},514:(e,t,o)=>{const n=o(896);const{request:s,throwApiError:r,throwNetworkError:a,ApiError:i}=o(852);const c=6e4;const l=72e5;const u=5*1024*1024;const p=3e3;const d=3e5;const f={WAITING:0,PROCESSING:1,SUCCESS:2,FAILED:3};function detectMode(e,t,o){if(e<=c&&o<=u){return"sentence"}if(e<=l&&t){return"flash"}return"file"}function authHeaders(e){return{"Content-Type":"application/json",Authorization:`Bearer ${e}`}}async function recognizeSentence(e){const{apiBase:t,token:o,url:c,filePath:l,lang:u="16k_zh",wordInfo:p=false}=e;const d={EngSerViceType:u,VoiceFormat:"wav",SubServiceType:2,WordInfo:p?1:0,ConvertNumMode:1};if(c){d.Url=c;d.SourceType=0}else if(l){const e=n.readFileSync(l);d.Data=e.toString("base64");d.DataLen=e.length;d.SourceType=1}else{throw new Error("Either url or filePath is required for sentence recognition")}try{const{status:e,data:n}=await s(`${t}/api/asr/sentence`,{method:"POST",headers:authHeaders(o)},d);if(e!==200||n.code!=="success"){r(e,n,"ASR sentence")}return{result:n.result,audioTime:n.audioTime,wordList:n.wordList||[],requestId:n.requestId,quota:n.quota}}catch(e){if(e instanceof i)throw e;a(e,t)}}async function recognizeFlash(e){const{apiBase:t,token:o,url:n,lang:c="16k_zh",speakerDiarization:l=false,speakerNumber:u=0}=e;if(!n){throw new Error("Flash recognition requires a URL (cannot use base64 data)")}const p={engine_type:c,voice_format:"wav",url:n,speaker_diarization:l?1:0,speaker_number:u,filter_dirty:0,filter_modal:0,filter_punc:0,convert_num_mode:1,word_info:1,first_channel_only:1};try{const{status:e,data:n}=await s(`${t}/api/asr/flash`,{method:"POST",headers:authHeaders(o)},p);if(e!==200||n.code!=="success"){r(e,n,"ASR flash")}return{flashResult:n.flash_result||[],audioDuration:n.audio_duration||0,requestId:n.request_id,quota:n.quota}}catch(e){if(e instanceof i)throw e;a(e,t)}}async function submitFileTask(e){const{apiBase:t,token:o,url:c,filePath:l,lang:p="16k_zh",speakerDiarization:d=false,speakerNumber:f=0}=e;const g={EngineModelType:p,ChannelNum:1,ResTextFormat:0,FilterDirty:0,FilterModal:0,FilterPunc:0,ConvertNumMode:1,SpeakerDiarization:d?1:0,SpeakerNumber:f};if(c){g.Url=c;g.SourceType=0}else if(l){const e=n.readFileSync(l);if(e.length>u){throw new Error(`File too large for base64 upload (${(e.length/1024/1024).toFixed(1)} MB). `+"Upload to COS first or use flash mode with a URL.")}g.Data=e.toString("base64");g.DataLen=e.length;g.SourceType=1}else{throw new Error("Either url or filePath is required for file recognition")}try{const{status:e,data:n}=await s(`${t}/api/asr/file`,{method:"POST",headers:authHeaders(o)},g);if(e!==200||n.code!=="success"){r(e,n,"ASR file submit")}return{taskId:n.taskId,requestId:n.requestId,quota:n.quota}}catch(e){if(e instanceof i)throw e;a(e,t)}}async function pollTaskResult(e){const{apiBase:t,token:o,taskId:n,pollIntervalMs:c=p,pollTimeoutMs:l=d,onProgress:u}=e;const g=Date.now();while(true){const e=Date.now()-g;if(e>l){throw new Error(`ASR task ${n} timed out after ${Math.round(e/1e3)}s. `+"The task may still complete — check later with: voxflow asr --task-id "+n)}try{const{status:a,data:i}=await s(`${t}/api/asr/result/${n}`,{method:"GET",headers:authHeaders(o)});if(a!==200||i.code!=="success"){r(a,i,"ASR poll")}const c=i.data;const l=c.Status;if(u)u(l,e);if(l===f.SUCCESS){return{result:c.Result,audioTime:c.AudioTime,status:l}}if(l===f.FAILED){throw new Error(`ASR task ${n} failed: ${c.Result||"Unknown error"}`)}}catch(o){if(o instanceof i)throw o;if(e+c<l){}else{a(o,t)}}await sleep(c)}}async function recognize(e){const{mode:t="auto",url:o,filePath:n,durationMs:s,fileSize:r=0}=e;const a=!!o;const i=t==="auto"?detectMode(s,a,r):t;switch(i){case"sentence":{const t=await recognizeSentence(e);return{mode:"sentence",result:t.result,audioTime:t.audioTime,wordList:t.wordList,quota:t.quota}}case"flash":{if(!o){throw new Error("Flash mode requires a URL. Upload the file to COS first, or use --mode auto.")}const t=await recognizeFlash(e);const n=(t.flashResult||[]).flatMap((e=>e.sentence_list?e.sentence_list.map((e=>e.text)):[e.text])).join("");return{mode:"flash",result:n,flashResult:t.flashResult,audioDuration:t.audioDuration,audioTime:(t.audioDuration||0)/1e3,quota:t.quota}}case"file":{const t=await submitFileTask(e);const o=await pollTaskResult({apiBase:e.apiBase,token:e.token,taskId:t.taskId,onProgress:e.onProgress});return{mode:"file",result:o.result,audioTime:o.audioTime,taskId:t.taskId,quota:t.quota}}default:throw new Error(`Unknown ASR mode: ${i}. Use: auto, sentence, flash, or file`)}}function sleep(e){return new Promise((t=>setTimeout(t,e)))}e.exports={recognize:recognize,recognizeSentence:recognizeSentence,recognizeFlash:recognizeFlash,submitFileTask:submitFileTask,pollTaskResult:pollTaskResult,detectMode:detectMode,SENTENCE_MAX_MS:c,FLASH_MAX_MS:l,BASE64_MAX_BYTES:u,TASK_STATUS:f}},388:(e,t,o)=>{const{execFile:n}=o(317);const s=o(928);const r=o(857);const a=o(896);function runCommand(e,t,o){return new Promise(((s,r)=>{n(e,t,{timeout:6e5,...o},((e,t,o)=>{if(e){e.stderr=o;e.stdout=t;r(e)}else{s({stdout:t,stderr:o})}}))}))}async function getMediaInfo(e){const t=s.resolve(e);if(!a.existsSync(t)){throw new Error(`File not found: ${t}`)}try{const{stdout:e}=await runCommand("ffprobe",["-v","error","-show_entries","format=duration","-show_entries","stream=codec_type,codec_name,sample_rate,channels","-of","json",t]);const o=JSON.parse(e);const n=o.streams||[];const s=o.format||{};const r=n.find((e=>e.codec_type==="audio"));const a=n.find((e=>e.codec_type==="video"));const i=parseFloat(s.duration);const c=isNaN(i)?0:Math.round(i*1e3);return{durationMs:c,hasVideo:!!a,hasAudio:!!r,audioCodec:r?r.codec_name:null,sampleRate:r?parseInt(r.sample_rate,10):null,channels:r?parseInt(r.channels,10):null}}catch(t){if(t.code==="ENOENT"){throw new Error("ffprobe not found. Please install ffmpeg:\n"+" macOS: brew install ffmpeg\n"+" Ubuntu: sudo apt install ffmpeg\n"+" Windows: https://ffmpeg.org/download.html")}throw new Error(`Failed to probe media file ${e}: ${t.message}`)}}async function extractAudioForAsr(e,t={}){const o=s.resolve(e);if(!a.existsSync(o)){throw new Error(`File not found: ${o}`)}const n=t.outputDir||r.tmpdir();const i=s.basename(o,s.extname(o));const c=s.join(n,`asr-${i}-${Date.now()}.wav`);try{await runCommand("ffmpeg",["-i",o,"-vn","-acodec","pcm_s16le","-ar","16000","-ac","1","-y",c])}catch(t){if(t.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg:\n"+" macOS: brew install ffmpeg\n"+" Ubuntu: sudo apt install ffmpeg\n"+" Windows: https://ffmpeg.org/download.html")}throw new Error(`Failed to extract audio from ${e}: ${t.stderr||t.message}`)}const l=a.statSync(c);const u=Math.round((l.size-44)/32);return{wavPath:c,durationMs:u,needsCleanup:true}}async function needsConversion(e){try{const t=await getMediaInfo(e);if(t.hasVideo)return true;if(t.audioCodec!=="pcm_s16le")return true;if(t.sampleRate!==16e3)return true;if(t.channels!==1)return true;return false}catch{return true}}e.exports={getMediaInfo:getMediaInfo,extractAudioForAsr:extractAudioForAsr,needsConversion:needsConversion}},56:e=>{function parseParagraphs(e){const t=e.split(/\n\s*\n/).map((e=>e.replace(/^\d+[.、)\]]\s*/,"").trim())).filter((e=>e.length>0));return t}function createSilence(e,t){const o=Math.floor(t*e);return Buffer.alloc(o*2,0)}function buildWav(e,t){const o=24e3;const n=16;const s=1;const r=n/8;const a=s*r;const i=o*a;const c=createSilence(t,o);let l=0;for(let t=0;t<e.length;t++){l+=e[t].length;if(t<e.length-1){l+=c.length}}const u=Buffer.alloc(44);u.write("RIFF",0);u.writeUInt32LE(36+l,4);u.write("WAVE",8);u.write("fmt ",12);u.writeUInt32LE(16,16);u.writeUInt16LE(1,20);u.writeUInt16LE(s,22);u.writeUInt32LE(o,24);u.writeUInt32LE(i,28);u.writeUInt16LE(a,32);u.writeUInt16LE(n,34);u.write("data",36);u.writeUInt32LE(l,40);const p=[u];for(let t=0;t<e.length;t++){p.push(e[t]);if(t<e.length-1){p.push(c)}}return{wav:Buffer.concat(p),duration:l/i}}function getFileExtension(e){switch(e){case"mp3":return".mp3";case"wav":return".wav";case"pcm":default:return".wav"}}function concatAudioBuffers(e,t,o){if(t==="mp3"){const t=Buffer.concat(e);const o=t.length/4e3;return{audio:t,duration:o}}if(t==="wav"){const t=e.map(extractPcmFromWav);return buildWav(t,o)}return buildWav(e,o)}function extractPcmFromWav(e){const t=Buffer.from("data");let o=12;while(o<e.length-8){if(e.subarray(o,o+4).equals(t)){const t=e.readUInt32LE(o+4);return e.subarray(o+8,o+8+t)}const n=e.readUInt32LE(o+4);o+=8+n}return e.subarray(44)}e.exports={parseParagraphs:parseParagraphs,createSilence:createSilence,buildWav:buildWav,concatAudioBuffers:concatAudioBuffers,getFileExtension:getFileExtension}},986:(e,t,o)=>{const n=o(611);const s=o(896);const r=o(928);const a=o(982);const i=o(785);const{TOKEN_PATH:c,getConfigDir:l,LOGIN_PAGE:u,AUTH_TIMEOUT_MS:p,API_BASE:d}=o(782);function readCachedToken(){try{const e=s.readFileSync(c,"utf8");const t=JSON.parse(e);if(!t.access_token)return null;const o=decodeJwtPayload(t.access_token);if(!o||!o.exp)return null;const n=Math.floor(Date.now()/1e3);if(o.exp-n<300)return null;return t}catch{return null}}function writeCachedToken(e){const t=l();s.mkdirSync(t,{recursive:true,mode:448});s.writeFileSync(c,JSON.stringify(e,null,2),{encoding:"utf8",mode:384})}function clearToken(){try{s.unlinkSync(c)}catch{}}function decodeJwtPayload(e){try{const t=e.split(".");if(t.length!==3)return null;const o=t[1].replace(/-/g,"+").replace(/_/g,"/");return JSON.parse(Buffer.from(o,"base64").toString("utf8"))}catch{return null}}async function getToken({api:e,force:t}={}){if(!t){const t=readCachedToken();if(t){const o=!e||e===t.api;if(o)return t.access_token}}return browserLogin(e||d)}function getTokenInfo(){const e=readCachedToken();if(!e)return null;const t=decodeJwtPayload(e.access_token);if(!t)return null;const o=Math.floor(Date.now()/1e3);return{email:t.email||e.email||"(unknown)",expiresAt:new Date(t.exp*1e3).toISOString(),remaining:t.exp-o,valid:t.exp-o>300,api:e.api||d}}function browserLogin(e){return new Promise(((t,s)=>{const r=a.randomBytes(16).toString("hex");let c=false;let l=null;function settle(o){if(c)return;c=true;const n=decodeJwtPayload(o);writeCachedToken({access_token:o,expires_at:n?.exp||0,email:n?.email||"",api:e,cached_at:(new Date).toISOString()});if(l){l.close();l=null}d.close();t(o)}const d=n.createServer(((e,t)=>{const o=new URL(e.url,`http://127.0.0.1`);if(o.pathname!=="/callback"){t.writeHead(404,{"Content-Type":"text/plain"});t.end("Not Found");return}const n=o.searchParams.get("token");const s=o.searchParams.get("state");if(s!==r){t.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});t.end("<h1>认证失败</h1><p>state 参数不匹配,请重试。</p>");return}if(!n){t.writeHead(400,{"Content-Type":"text/html; charset=utf-8"});t.end("<h1>认证失败</h1><p>未收到 token,请重试。</p>");return}t.writeHead(200,{"Content-Type":"text/html; charset=utf-8"});t.end(`<!DOCTYPE html>\n<html><head><meta charset="utf-8"><title>登录成功</title></head>\n<body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f0fdf4">\n<div style="text-align:center">\n<h1 style="color:#16a34a;font-size:2rem">登录成功</h1>\n<p style="color:#666;margin-top:0.5rem">已授权 voxflow CLI,可以关闭此窗口。</p>\n</div></body></html>`);settle(n)}));d.listen(0,"127.0.0.1",(async()=>{const e=d.address().port;const t=`${u}?state=${r}&callback_port=${e}`;console.log("\n[33m🔐 需要登录。正在打开浏览器...[0m");console.log(` 若未自动打开: ${t}\n`);let n=false;try{const e=(await o.e(935).then(o.bind(o,935))).default;const s=await e(t);if(s&&typeof s.on==="function"){s.on("error",(()=>{n=true;console.log("[31m 浏览器打开失败,请手动复制上面的链接到浏览器。[0m\n");startStdinListener()}))}}catch{n=true;console.log("[31m 浏览器打开失败,请手动复制上面的链接到浏览器。[0m\n");startStdinListener()}function startStdinListener(){if(c||l||!process.stdin.isTTY)return;console.log(" [36m登录后网页会显示授权码,粘贴到此处回车即可[0m");l=i.createInterface({input:process.stdin,output:process.stdout,terminal:false});process.stdout.write(" > Token: ");l.on("line",(e=>{const t=e.trim();if(!t)return;const o=decodeJwtPayload(t);if(!o){console.log(" [31m无效的 token,请重新粘贴完整的授权码。[0m");process.stdout.write(" > Token: ");return}const n=Math.floor(Date.now()/1e3);if(o.exp&&o.exp<n){console.log(" [31mtoken 已过期,请重新登录获取。[0m");process.stdout.write(" > Token: ");return}console.log(`\n[32m✓ 授权成功 (${o.email||"user"})[0m`);settle(t)}))}}));const f=setTimeout((()=>{if(!c){c=true;if(l){l.close();l=null}d.close();s(new Error(`登录超时 (${p/1e3}s)。请重试: voxflow login`))}}),p);d.on("close",(()=>clearTimeout(f)));d.on("error",(e=>{if(!c){c=true;if(l){l.close();l=null}s(new Error(`本地服务器启动失败: ${e.message}`))}}))}))}e.exports={getToken:getToken,clearToken:clearToken,getTokenInfo:getTokenInfo}},782:(e,t,o)=>{const n=o(928);const s=o(857);const r="https://api.voxflow.studio";const a="https://iwkonytsjysszmafqchh.supabase.co";const i="sb_publishable_TEh6H4K9OWXUNfWSeBKXlQ_hg7Zzm6b";const c="voxflow";function getConfigDir(){if(process.platform==="win32"){return n.join(process.env.APPDATA||n.join(s.homedir(),"AppData","Roaming"),c)}const e=process.env.XDG_CONFIG_HOME||n.join(s.homedir(),".config");return n.join(e,c)}const l=n.join(getConfigDir(),"token.json");const u={voice:"v-female-R2s4N9qJ",paragraphs:5,speed:1,silence:.8};const p={template:"interview",exchanges:8,length:"medium",style:"professional",speakers:2,silence:.5,speed:1};const d={voice:"v-female-R2s4N9qJ",speed:1,volume:1,pitch:0};const f={voice:"v-female-R2s4N9qJ",speed:1,silence:.8};const g={voice:"v-female-R2s4N9qJ",speed:1,toleranceMs:50,ducking:.2};const m={lang:"16k_zh",mode:"auto",format:"srt",pollIntervalMs:3e3,pollTimeoutMs:3e5,engine:"auto",model:"base"};const h={batchSize:10,temperature:.3,maxTokens:2e3};const w={batchSize:10,speed:1};const x="https://voxflow.studio";const v=`${r}/cli-auth.html`;const S=18e4;e.exports={API_BASE:r,WEB_BASE:x,SUPABASE_URL:a,SUPABASE_ANON_KEY:i,TOKEN_PATH:l,getConfigDir:getConfigDir,DEFAULTS:u,STORY_DEFAULTS:u,PODCAST_DEFAULTS:p,SYNTHESIZE_DEFAULTS:d,NARRATE_DEFAULTS:f,DUB_DEFAULTS:g,ASR_DEFAULTS:m,TRANSLATE_DEFAULTS:h,VIDEO_TRANSLATE_DEFAULTS:w,LOGIN_PAGE:v,AUTH_TIMEOUT_MS:S}},567:(e,t,o)=>{const n=o(896);const s=o(928);const r=o(611);const a=o(692);const{request:i,throwApiError:c,throwNetworkError:l,ApiError:u}=o(852);const p={".wav":"audio/wav",".mp3":"audio/mpeg",".ogg":"audio/ogg",".m4a":"audio/x-m4a",".mp4":"video/mp4",".webm":"video/webm",".mov":"video/quicktime",".avi":"video/x-msvideo",".mkv":"video/x-matroska",".flac":"audio/flac"};function getMimeType(e){const t=s.extname(e).toLowerCase();return p[t]||"application/octet-stream"}async function uploadFileToCos(e,t,o){const r=s.resolve(e);if(!n.existsSync(r)){throw new Error(`File not found: ${r}`)}const a=n.statSync(r);const p=s.basename(r);const d=getMimeType(r);const f=a.size;let g;try{const{status:e,data:n}=await i(`${t}/api/file-upload/get-upload-url`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`}},{filename:p,fileType:d,fileSize:f});if(e!==200||n.code!=="success"){c(e,n,"Get upload URL")}g=n.data}catch(e){if(e instanceof u)throw e;l(e,t)}const{uploadUrl:m,key:h,bucket:w,region:x}=g;await putFile(m,r,d);let v;try{v=await getSignedDownloadUrl(t,o,h)}catch{v=`https://${w}.cos.${x}.myqcloud.com/${h}`}return{cosUrl:v,key:h}}function putFile(e,t,o){return new Promise(((s,i)=>{const c=new URL(e);const l=c.protocol==="https:"?a:r;const u=n.statSync(t).size;const p={hostname:c.hostname,port:c.port||(c.protocol==="https:"?443:80),path:c.pathname+c.search,method:"PUT",headers:{"Content-Type":o,"Content-Length":u}};const d=l.request(p,(e=>{const t=[];e.on("data",(e=>t.push(e)));e.on("end",(()=>{if(e.statusCode>=200&&e.statusCode<300){s()}else{const o=Buffer.concat(t).toString("utf8");i(new Error(`COS upload failed (${e.statusCode}): ${o.slice(0,300)}`))}}))}));d.on("error",(e=>i(new Error(`COS upload network error: ${e.message}`))));d.setTimeout(3e5,(()=>{d.destroy();i(new Error("COS upload timeout (5 min)"))}));const f=n.createReadStream(t);f.pipe(d);f.on("error",(e=>{d.destroy();i(new Error(`Failed to read file for upload: ${e.message}`))}))}))}async function getSignedDownloadUrl(e,t,o){const{status:n,data:s}=await i(`${e}/api/file-upload/get-download-url`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{key:o});if(n!==200||s.code!=="success"){throw new Error(`Failed to get download URL: ${s.message||n}`)}return s.data.downloadUrl}e.exports={uploadFileToCos:uploadFileToCos,getSignedDownloadUrl:getSignedDownloadUrl,getMimeType:getMimeType}},297:(e,t,o)=>{const{execFile:n}=o(317);const s=o(928);const r=o(896);function runCommand(e,t,o){return new Promise(((s,r)=>{n(e,t,{timeout:3e5,...o},((e,t,o)=>{if(e){e.stderr=o;e.stdout=t;r(e)}else{s({stdout:t,stderr:o})}}))}))}async function checkFfmpeg(){try{const{stdout:e}=await runCommand("ffmpeg",["-version"]);const t=e.match(/ffmpeg version (\S+)/);const o=t?t[1]:"unknown";let n=false;try{await runCommand("ffprobe",["-version"]);n=true}catch{}return{available:true,version:o,ffprobeAvailable:n}}catch{return{available:false}}}async function getAudioDuration(e){const t=s.resolve(e);try{const{stdout:e}=await runCommand("ffprobe",["-v","error","-show_entries","format=duration","-of","default=noprint_wrappers=1:nokey=1",t]);const o=parseFloat(e.trim());if(isNaN(o)){throw new Error(`Could not parse duration from ffprobe output: "${e.trim()}"`)}return Math.round(o*1e3)}catch(t){if(t.code==="ENOENT"){throw new Error("ffprobe not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to get duration of ${e}: ${t.message}`)}}async function extractAudio(e,t){const o=s.resolve(e);const n=s.resolve(t);try{await runCommand("ffmpeg",["-i",o,"-vn","-acodec","pcm_s16le","-ar","24000","-ac","1","-y",n]);return n}catch(t){if(t.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to extract audio from ${e}: ${t.stderr||t.message}`)}}async function mergeAudioVideo(e,t,o){const n=s.resolve(e);const r=s.resolve(t);const a=s.resolve(o);try{await runCommand("ffmpeg",["-i",n,"-i",r,"-c:v","copy","-map","0:v:0","-map","1:a:0","-shortest","-y",a]);return a}catch(e){if(e.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to merge audio/video: ${e.stderr||e.message}`)}}async function mixWithBgm(e,t,o,n={}){const r=n.ducking??.2;const a=s.resolve(e);const i=s.resolve(t);const c=s.resolve(o);try{await runCommand("ffmpeg",["-i",a,"-i",i,"-filter_complex",`[1:a]volume=${r}[bgm_low];`+`[0:a][bgm_low]amix=inputs=2:duration=first:dropout_transition=2[out]`,"-map","[out]","-acodec","pcm_s16le","-ar","24000","-ac","1","-y",c]);return c}catch(e){if(e.code==="ENOENT"){throw new Error("ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html")}throw new Error(`Failed to mix audio with BGM: ${e.stderr||e.message}`)}}async function warnIfMissingFfmpeg(e,t){const o=await checkFfmpeg();if(o.available)return o;const n=s.join(e,".ffmpeg-hint-shown");try{if(r.existsSync(n))return o}catch{}const a={dub:"video merging (--video), BGM mixing (--bgm), speed adjustment (--speed-auto)",asr:"audio format conversion, video audio extraction"};const i=a[t]||"audio/video processing";console.log("\n[33m"+`[hint] ffmpeg not found — needed for ${i}.\n`+" Install: brew install ffmpeg (macOS) / sudo apt install ffmpeg (Linux)\n"+" Without ffmpeg, some features will be unavailable.[0m\n");try{r.mkdirSync(e,{recursive:true});r.writeFileSync(n,(new Date).toISOString(),"utf8")}catch{}return o}e.exports={checkFfmpeg:checkFfmpeg,getAudioDuration:getAudioDuration,extractAudio:extractAudio,mergeAudioVideo:mergeAudioVideo,mixWithBgm:mixWithBgm,warnIfMissingFfmpeg:warnIfMissingFfmpeg}},852:(e,t,o)=>{const n=o(611);const s=o(692);class ApiError extends Error{constructor(e,t,o){super(e);this.name="ApiError";this.code=t;this.status=o}}function throwApiError(e,t,o){if(e===401){throw new ApiError(`Token expired or invalid. Run: voxflow login`,"token_expired",401)}if(e===429||t&&t.code==="quota_exceeded"){throw new ApiError(`Daily quota exceeded. Your quota resets tomorrow. Check: voxflow status`,"quota_exceeded",429)}if(e>=500){throw new ApiError(`Server error (${e}). Please try again later.`,"server_error",e)}const n=t?.message||t?.code||JSON.stringify(t);throw new ApiError(`${o} failed (${e}): ${n}`,"api_error",e)}function throwNetworkError(e,t){const o=e.code||"";if(o==="ECONNREFUSED"||o==="ENOTFOUND"||o==="ETIMEDOUT"){throw new ApiError(`Cannot reach API server at ${t}. Check your internet connection or try --api <url>`,"network_error",0)}throw e}function request(e,t,o){return new Promise(((r,a)=>{const i=new URL(e);const c=i.protocol==="https:"?s:n;const l=c.request(i,t,(e=>{const t=[];e.on("data",(e=>t.push(e)));e.on("end",(()=>{const o=Buffer.concat(t).toString("utf8");try{r({status:e.statusCode,data:JSON.parse(o)})}catch{a(new Error(`Non-JSON response (${e.statusCode}): ${o.slice(0,200)}`))}}))}));l.on("error",(e=>a(e)));l.setTimeout(6e4,(()=>{l.destroy();a(new Error("Request timeout (60s)"))}));if(o)l.write(JSON.stringify(o));l.end()}))}e.exports={request:request,ApiError:ApiError,throwApiError:throwApiError,throwNetworkError:throwNetworkError}},133:(e,t,o)=>{const{request:n,throwApiError:s,throwNetworkError:r}=o(852);async function chatCompletion({apiBase:e,token:t,messages:o,temperature:a=.3,maxTokens:i=2e3}){let c,l;try{({status:c,data:l}=await n(`${e}/api/llm/chat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}},{messages:o,temperature:a,max_tokens:i}))}catch(t){r(t,e)}if(c!==200||l.code!=="success"){s(c,l,"LLM chat")}return{content:l.content,usage:l.usage,quota:l.quota}}async function detectLanguage({apiBase:e,text:t}){let o,s;try{({status:o,data:s}=await n(`${e}/api/lang-detect/detect`,{method:"POST",headers:{"Content-Type":"application/json"}},{text:t.slice(0,200)}))}catch{return"auto"}if(o===200&&s.code==="success"){return s.language}return"auto"}e.exports={chatCompletion:chatCompletion,detectLanguage:detectLanguage}},384:(e,t,o)=>{const{spawn:n}=o(317);const s=o(928);const r=o(857);const a=o(896);async function checkRecAvailable(){return new Promise((e=>{const t=n("rec",["--version"],{stdio:"pipe"});let o="";t.stdout.on("data",(e=>{o+=e}));t.stderr.on("data",(e=>{o+=e}));t.on("error",(()=>{e({available:false,error:"rec (sox) not found. Please install sox:\n"+" macOS: brew install sox\n"+" Ubuntu: sudo apt install sox\n"+" Windows: https://sourceforge.net/projects/sox/"})}));t.on("close",(t=>{e({available:t===0||o.length>0})}))}))}function recordMic(e={}){const{outputDir:t=r.tmpdir(),maxSeconds:o=300,silenceThreshold:i=0}=e;const c=s.join(t,`mic-${Date.now()}.wav`);return new Promise(((e,t)=>{const s=["-r","16000","-c","1","-b","16","-e","signed-integer",c,"trim","0",String(o)];if(i>0){s.push("silence","1","0.1","1%","1",String(i),"1%")}const r=n("rec",s,{stdio:["pipe","pipe","pipe"]});let l="";r.stderr.on("data",(e=>{l+=e.toString()}));r.on("error",(e=>{if(e.code==="ENOENT"){t(new Error("rec (sox) not found. Please install sox:\n"+" macOS: brew install sox\n"+" Ubuntu: sudo apt install sox\n"+" Windows: https://sourceforge.net/projects/sox/"))}else{t(new Error(`Microphone recording failed: ${e.message}`))}}));let u="timeout";r.on("close",(o=>{if(!a.existsSync(c)){return t(new Error(`Recording failed — no output file created.\n${l.slice(0,500)}`))}const n=a.statSync(c);if(n.size<100){a.unlinkSync(c);return t(new Error("Recording produced an empty file. Check that your microphone is connected and accessible."))}const s=Math.round((n.size-44)/32);e({wavPath:c,durationMs:s,stopped:u})}));const stopRecording=()=>{u="user";r.kill("SIGTERM")};if(process.stdin.isTTY){process.stdin.setRawMode(true);process.stdin.resume();const onKey=e=>{if(e[0]===13||e[0]===10||e[0]===113){u="user";process.stdin.setRawMode(false);process.stdin.removeListener("data",onKey);process.stdin.pause();r.kill("SIGTERM")}if(e[0]===3){u="user";process.stdin.setRawMode(false);process.stdin.removeListener("data",onKey);process.stdin.pause();r.kill("SIGTERM")}};process.stdin.on("data",onKey);r.on("close",(()=>{try{process.stdin.removeListener("data",onKey);if(process.stdin.isTTY){process.stdin.setRawMode(false)}process.stdin.pause()}catch{}}))}r._stopRecording=stopRecording}))}e.exports={recordMic:recordMic,checkRecAvailable:checkRecAvailable}},339:e=>{function startSpinner(e){const t=["|","/","-","\\"];let o=0;process.stdout.write(e+" "+t[0]);const n=setInterval((()=>{o=(o+1)%t.length;process.stdout.write("\b"+t[o])}),120);return{stop(e){clearInterval(n);process.stdout.write("\b"+e+"\n")}}}e.exports={startSpinner:startSpinner}},813:e=>{function parseTimestamp(e){const t=e.trim().match(/^(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})$/);if(!t){throw new Error(`Invalid SRT timestamp: "${e}"`)}const[,o,n,s,r]=t;return parseInt(o,10)*36e5+parseInt(n,10)*6e4+parseInt(s,10)*1e3+parseInt(r,10)}function formatTimestamp(e){if(e<0)e=0;const t=Math.floor(e/36e5);e%=36e5;const o=Math.floor(e/6e4);e%=6e4;const n=Math.floor(e/1e3);const s=e%1e3;return String(t).padStart(2,"0")+":"+String(o).padStart(2,"0")+":"+String(n).padStart(2,"0")+","+String(s).padStart(3,"0")}function parseSrt(e){if(!e||e.trim().length===0){return[]}const t=[];const o=e.replace(/\r\n/g,"\n").replace(/\r/g,"\n");const n=o.split(/\n\s*\n/).filter((e=>e.trim().length>0));for(const e of n){const o=e.trim().split("\n");if(o.length<2)continue;let n=0;let s;const r=o[0].trim();if(/^\d+$/.test(r)){s=parseInt(r,10);n=1}else{s=t.length+1}if(n>=o.length)continue;const a=o[n].trim();const i=a.match(/^(\d{1,2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{1,2}:\d{2}:\d{2}[,.]\d{3})/);if(!i)continue;const c=parseTimestamp(i[1]);const l=parseTimestamp(i[2]);n++;const u=o.slice(n).filter((e=>e.trim().length>0));if(u.length===0)continue;const p=u.join("\n");let d;let f=p;const g=p.match(/^\[Speaker:\s*([^\]]+)\]\s*/i);if(g){d=g[1].trim();f=p.slice(g[0].length)}if(f.trim().length===0)continue;t.push({id:s,startMs:c,endMs:l,text:f.trim(),...d?{speakerId:d}:{}})}t.sort(((e,t)=>e.startMs-t.startMs));return t}function formatSrt(e){return e.map(((e,t)=>{const o=e.id||t+1;const n=formatTimestamp(e.startMs);const s=formatTimestamp(e.endMs);const r=e.speakerId?`[Speaker: ${e.speakerId}] `:"";return`${o}\n${n} --\x3e ${s}\n${r}${e.text}`})).join("\n\n")+"\n"}function buildCaptionsFromFlash(e){const t=[];let o=1;for(const n of e){const e=n.sentence_list||[];for(const n of e){const e={id:o++,startMs:n.start_time||0,endMs:n.end_time||0,text:(n.text||"").trim()};if(n.speaker_id!==undefined&&n.speaker_id!==null){e.speakerId=`Speaker${n.speaker_id}`}if(e.text.length>0){t.push(e)}}}return t}function buildCaptionsFromSentence(e,t,o){if(!e||e.trim().length===0)return[];if(o&&o.length>0){return buildCaptionsFromWordList(o,e)}return[{id:1,startMs:0,endMs:Math.round(t*1e3),text:e.trim()}]}function buildCaptionsFromWordList(e,t){if(!e||e.length===0){return t?[{id:1,startMs:0,endMs:0,text:t}]:[]}const o=500;const n=5e3;const s=15e3;const r=/[.!?。!?…]+$/;const getWord=e=>e.word||e.Word||"";const getStart=e=>e.startTime??e.StartTime??0;const getEnd=e=>e.endTime??e.EndTime??0;const a=e.slice(0,10).map(getWord).join("");const i=(a.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g)||[]).length;const c=i<a.length*.3;const l=c?" ":"";const u=[];let p=[];let d=getStart(e[0]);let f=d;function flushCaption(){if(p.length===0)return;const e=p.join(l).trim();if(e.length>0){u.push({id:u.length+1,startMs:d,endMs:f,text:e})}p=[]}for(let t=0;t<e.length;t++){const a=e[t];const i=getStart(a);const c=getEnd(a);const l=i-f;const u=i-d;if(l>o&&p.length>0){flushCaption();d=i}else if(p.length>0&&u>n&&r.test(p[p.length-1])){flushCaption();d=i}else if(p.length>0&&u>s){flushCaption();d=i}p.push(getWord(a));f=c||f}flushCaption();return u}function buildCaptionsFromFile(e,t){if(!e||e.trim().length===0)return[];if(/^\d+\s*\n\d{2}:\d{2}:\d{2}[,.]\d{3}\s*-->/.test(e.trim())){return parseSrt(e)}return[{id:1,startMs:0,endMs:Math.round(t*1e3),text:e.trim()}]}function formatPlainText(e,t={}){return e.map((e=>{const o=t.includeSpeakers&&e.speakerId?`[${e.speakerId}] `:"";return`${o}${e.text}`})).join("\n")+"\n"}function formatJson(e){return JSON.stringify(e,null,2)+"\n"}e.exports={parseSrt:parseSrt,formatSrt:formatSrt,parseTimestamp:parseTimestamp,formatTimestamp:formatTimestamp,buildCaptionsFromFlash:buildCaptionsFromFlash,buildCaptionsFromSentence:buildCaptionsFromSentence,buildCaptionsFromWordList:buildCaptionsFromWordList,buildCaptionsFromFile:buildCaptionsFromFile,formatPlainText:formatPlainText,formatJson:formatJson}},907:(e,t,o)=>{const{createSilence:n,buildWav:s}=o(56);const r=24e3;const a=2;const i=r*a/1e3;function msToBytes(e){const t=Math.round(e*i);return t-t%a}function buildTimelinePcm(e,t){if(!e||e.length===0){return{pcm:Buffer.alloc(0),durationMs:0}}const o=Math.max(...e.map((e=>e.endMs)));const n=t||o;const s=msToBytes(n);const r=Buffer.alloc(s,0);for(const t of e){const e=msToBytes(t.startMs);const o=msToBytes(t.endMs)-e;const n=Math.min(t.audioBuffer.length,o,s-e);if(n>0&&e<s){t.audioBuffer.copy(r,e,0,n)}}return{pcm:r,durationMs:n}}function buildTimelineAudio(e,t){const{pcm:o,durationMs:n}=buildTimelinePcm(e,t);if(o.length===0){return{wav:Buffer.alloc(0),duration:0}}const{wav:r}=s([o],0);return{wav:r,duration:n/1e3}}e.exports={buildTimelinePcm:buildTimelinePcm,buildTimelineAudio:buildTimelineAudio,msToBytes:msToBytes,SAMPLE_RATE:r,BYTES_PER_SAMPLE:a,BYTES_PER_MS:i}},126:(e,t,o)=>{const n=o(896);const s=o(928);const{execFileSync:r}=o(317);const a={"16k_zh":"Chinese","16k_en":"English","16k_zh_en":"Chinese","16k_ja":"Japanese","16k_ko":"Korean","16k_zh_dialect":"Chinese","8k_zh":"Chinese","8k_en":"English",zh:"Chinese",en:"English",ja:"Japanese",ko:"Korean",auto:"auto"};function checkWhisperAvailable(){try{const e=resolveWhisperModule();if(e){return{available:true}}return{available:false,error:"nodejs-whisper is not installed.\n"+"Install it with: npm install -g nodejs-whisper\n"+"Then download a model: npx nodejs-whisper download"}}catch{return{available:false,error:"nodejs-whisper is not installed.\n"+"Install it with: npm install -g nodejs-whisper\n"+"Then download a model: npx nodejs-whisper download"}}}function resolveWhisperModule(){try{return require.resolve("nodejs-whisper")}catch{}try{const e=r("npm",["root","-g"],{encoding:"utf8"}).trim();const t=s.join(e,"nodejs-whisper");if(n.existsSync(t)){return t}}catch{}return null}function loadWhisperModule(){const e=resolveWhisperModule();if(!e){throw new Error("nodejs-whisper is not installed.\n"+"Install: npm install -g nodejs-whisper\n"+"Then: npx nodejs-whisper download")}const t=require(e);return t.nodewhisper||t.default||t}async function transcribeLocal(e,t={}){const{model:o="base",lang:s="16k_zh"}=t;if(!n.existsSync(e)){throw new Error(`WAV file not found: ${e}`)}const r=loadWhisperModule();const i=a[s]||a["auto"]||"auto";const c=o;await r(e,{modelName:c,autoDownloadModelName:c,removeWavFileAfterTranscription:false,whisperOptions:{outputInJson:true,outputInSrt:false,outputInVtt:false,outputInTxt:false,wordTimestamps:true,splitOnWord:true,language:i==="auto"?undefined:i}});const l=e+".json";if(!n.existsSync(l)){const t=e.replace(/\.wav$/i,"");const o=[t+".json",e+".json"];const s=o.find((e=>n.existsSync(e)));if(!s){throw new Error("Whisper completed but no JSON output found.\n"+`Expected: ${l}\n`+"Ensure nodejs-whisper is correctly installed.")}}const u=n.readFileSync(l,"utf8");const p=JSON.parse(u);const d=p.transcription||p.segments||[];const f=parseWhisperOutput(d);cleanupWhisperFiles(e);return f}function parseWhisperOutput(e){if(!e||!Array.isArray(e))return[];let t=0;const o=[];for(const n of e){const e=(n.text||"").trim();if(!e)continue;t++;let s=0;let r=0;if(n.timestamps){s=parseTimestamp(n.timestamps.from);r=parseTimestamp(n.timestamps.to)}else if(n.offsets){s=n.offsets.from||0;r=n.offsets.to||0}else if(typeof n.start==="number"){s=Math.round(n.start*1e3);r=Math.round(n.end*1e3)}o.push({id:t,startMs:s,endMs:r,text:e})}return o}function parseTimestamp(e){if(!e||typeof e!=="string")return 0;const t=e.match(/^(\d+):(\d+):(\d+)\.(\d+)$/);if(!t)return 0;const o=parseInt(t[1],10);const n=parseInt(t[2],10);const s=parseInt(t[3],10);const r=parseInt(t[4].padEnd(3,"0").slice(0,3),10);return(o*3600+n*60+s)*1e3+r}function cleanupWhisperFiles(e){const t=[".json",".srt",".vtt",".txt",".lrc",".wts"];for(const o of t){const t=e+o;try{if(n.existsSync(t)){n.unlinkSync(t)}}catch{}}}e.exports={checkWhisperAvailable:checkWhisperAvailable,transcribeLocal:transcribeLocal,parseWhisperOutput:parseWhisperOutput,parseTimestamp:parseTimestamp,LANG_MAP:a}},317:e=>{"use strict";e.exports=require("child_process")},982:e=>{"use strict";e.exports=require("crypto")},896:e=>{"use strict";e.exports=require("fs")},611:e=>{"use strict";e.exports=require("http")},692:e=>{"use strict";e.exports=require("https")},573:e=>{"use strict";e.exports=require("node:buffer")},421:e=>{"use strict";e.exports=require("node:child_process")},24:e=>{"use strict";e.exports=require("node:fs")},455:e=>{"use strict";e.exports=require("node:fs/promises")},161:e=>{"use strict";e.exports=require("node:os")},760:e=>{"use strict";e.exports=require("node:path")},708:e=>{"use strict";e.exports=require("node:process")},136:e=>{"use strict";e.exports=require("node:url")},975:e=>{"use strict";e.exports=require("node:util")},857:e=>{"use strict";e.exports=require("os")},928:e=>{"use strict";e.exports=require("path")},785:e=>{"use strict";e.exports=require("readline")},330:e=>{"use strict";e.exports=JSON.parse('{"name":"voxflow","version":"1.5.2","description":"AI audio content creation CLI — stories, podcasts, narration, dubbing, transcription, translation, and video translation with TTS","bin":{"voxflow":"./dist/index.js"},"files":["dist/index.js","dist/935.index.js","README.md"],"engines":{"node":">=18.0.0"},"dependencies":{"open":"^10.0.0"},"keywords":["tts","story","podcast","ai","audio","text-to-speech","voice","narration","dubbing","synthesize","voices","document","translate","subtitle","srt","transcribe","asr","video-translate","video","voxflow"],"scripts":{"build":"ncc build bin/voxflow.js -o dist --minify","prepublishOnly":"npm run build","test":"node --test tests/*.test.js"},"author":"gonghaoran","license":"UNLICENSED","homepage":"https://voxflow.studio","repository":{"type":"git","url":"https://github.com/VoxFlowStudio/FlowStudio","directory":"cli"},"publishConfig":{"access":"public"},"devDependencies":{"@vercel/ncc":"^0.38.4"}}')}};var t={};function __nccwpck_require__(o){var n=t[o];if(n!==undefined){return n.exports}var s=t[o]={exports:{}};var r=true;try{e[o](s,s.exports,__nccwpck_require__);r=false}finally{if(r)delete t[o]}return s.exports}__nccwpck_require__.m=e;(()=>{__nccwpck_require__.d=(e,t)=>{for(var o in t){if(__nccwpck_require__.o(t,o)&&!__nccwpck_require__.o(e,o)){Object.defineProperty(e,o,{enumerable:true,get:t[o]})}}}})();(()=>{__nccwpck_require__.f={};__nccwpck_require__.e=e=>Promise.all(Object.keys(__nccwpck_require__.f).reduce(((t,o)=>{__nccwpck_require__.f[o](e,t);return t}),[]))})();(()=>{__nccwpck_require__.u=e=>""+e+".index.js"})();(()=>{__nccwpck_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})();(()=>{__nccwpck_require__.r=e=>{if(typeof Symbol!=="undefined"&&Symbol.toStringTag){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}Object.defineProperty(e,"__esModule",{value:true})}})();if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";(()=>{var e={792:1};var installChunk=t=>{var o=t.modules,n=t.ids,s=t.runtime;for(var r in o){if(__nccwpck_require__.o(o,r)){__nccwpck_require__.m[r]=o[r]}}if(s)s(__nccwpck_require__);for(var a=0;a<n.length;a++)e[n[a]]=1};__nccwpck_require__.f.require=(t,o)=>{if(!e[t]){if(true){installChunk(require("./"+__nccwpck_require__.u(t)))}else e[t]=1}}})();var o={};const{run:n}=__nccwpck_require__(6);n().catch((e=>{console.error(`\nFatal error: ${e.message}`);process.exit(1)}));module.exports=o})();
|
package/package.json
CHANGED