1+ import { existsSync , mkdirSync , writeFileSync } from 'node:fs' ;
2+ import { resolve , dirname } from 'node:path' ;
13import { Client } from "../client" ;
24import { speechEndpoint , voicesEndpoint } from "../../client/endpoints" ;
35import { SpeechRequest , SpeechResponse , VoiceListResponse } from "../../types/api" ;
@@ -7,6 +9,21 @@ import { ExitCode } from "../../errors/codes";
79import { toMerged } from "es-toolkit/object" ;
810import { ModelPartial } from "../types" ;
911
12+ function hexToBuffer ( hex : string ) : Buffer {
13+ if ( ! / ^ [ 0 - 9 a - f A - F ] * $ / . test ( hex ) ) {
14+ throw new SDKError ( 'API returned invalid audio data (not valid hex).' , ExitCode . GENERAL ) ;
15+ }
16+ if ( hex . length % 2 !== 0 ) {
17+ throw new SDKError ( 'API returned truncated audio data (odd-length hex string).' , ExitCode . GENERAL ) ;
18+ }
19+ return Buffer . from ( hex , 'hex' ) ;
20+ }
21+
22+ function defaultFilename ( prefix : string , ext : string ) : string {
23+ const ts = new Date ( ) . toISOString ( ) . slice ( 0 , 19 ) . replace ( / [ T : ] / g, '-' ) ;
24+ return `${ prefix } _${ ts } .${ ext } ` ;
25+ }
26+
1027export class SpeechSDK extends Client {
1128 async synthesize ( request : ModelPartial < SpeechRequest > & { stream : true } ) : Promise < AsyncGenerator < SpeechResponse > > ;
1229 async synthesize ( request : ModelPartial < SpeechRequest > ) : Promise < SpeechResponse > ;
@@ -56,6 +73,38 @@ export class SpeechSDK extends Client {
5673 return voices ;
5774 }
5875
76+ /**
77+ * Save synthesized speech audio to a file. Decodes the hex-encoded audio
78+ * from the API response and writes it to disk. Creates intermediate
79+ * directories as needed.
80+ *
81+ * @param response — The response from `synthesize()`.
82+ * @param outPath — Target file path. Defaults to `speech_<timestamp>.mp3`.
83+ * @param ext — File extension (default: `"mp3"`).
84+ * @returns The absolute path of the saved file.
85+ */
86+ save ( response : SpeechResponse , outPath ?: string , ext = 'mp3' ) : string {
87+ const dest = resolve ( outPath || defaultFilename ( 'speech' , ext ) ) ;
88+ const audioHex = response . data . audio ;
89+ if ( ! audioHex ) {
90+ throw new SDKError ( 'API response missing audio data.' , ExitCode . GENERAL ) ;
91+ }
92+
93+ const dir = dirname ( dest ) ;
94+ if ( ! existsSync ( dir ) ) mkdirSync ( dir , { recursive : true } ) ;
95+
96+ try {
97+ writeFileSync ( dest , hexToBuffer ( audioHex ) ) ;
98+ } catch ( err ) {
99+ if ( ( err as NodeJS . ErrnoException ) . code === 'ENOSPC' ) {
100+ throw new SDKError ( 'Disk full — cannot write audio file.' , ExitCode . GENERAL ) ;
101+ }
102+ throw err ;
103+ }
104+
105+ return dest ;
106+ }
107+
59108 private validateParams ( params : Partial < SpeechRequest > ) : SpeechRequest {
60109 if ( ! params . text ) {
61110 throw new SDKError ( 'text is required' , ExitCode . USAGE ) ;
0 commit comments