diff --git a/src/commands/speech/synthesize.ts b/src/commands/speech/synthesize.ts index d095201b..0e17f6a9 100644 --- a/src/commands/speech/synthesize.ts +++ b/src/commands/speech/synthesize.ts @@ -6,6 +6,7 @@ import { speechEndpoint } from '../../client/endpoints'; import { detectOutputFormat, formatOutput, dryRun } from '../../output/formatter'; import { saveAudioOutput } from '../../output/audio'; import { writeFileSync } from 'fs'; +import { extname } from 'path'; import { readTextFromPathOrStdin } from '../../utils/fs'; import { T2A_FORMATS, formatList, validateAudioFormat, validateT2AStreaming, t2aDefaultSampleRate } from '../../utils/audio-formats'; import { pipeAudioStream } from '../../utils/audio-stream'; @@ -136,7 +137,7 @@ export default defineCommand({ if (subtitleArray?.length) { // Convert to SRT format (API returns time in milliseconds) - const subtitlePath = outPath.replace(/\.[^.]+$/, '') + '.srt'; + const subtitlePath = outPath.slice(0, outPath.length - extname(outPath).length) + '.srt'; const srtContent = subtitleArray .map((s, i) => { // API already returns milliseconds, use directly diff --git a/test/commands/speech/synthesize.test.ts b/test/commands/speech/synthesize.test.ts index 4e085cbd..58f18e28 100644 --- a/test/commands/speech/synthesize.test.ts +++ b/test/commands/speech/synthesize.test.ts @@ -1,4 +1,8 @@ -import { describe, it, expect } from 'bun:test'; +import { describe, it, expect, spyOn } from 'bun:test'; +import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createMockServer, jsonResponse } from '../../helpers/mock-server'; import { default as synthesizeCommand } from '../../../src/commands/speech/synthesize'; describe('speech synthesize command', () => { @@ -399,6 +403,47 @@ describe('speech synthesize format validation', () => { }); }); +describe('speech synthesize subtitle paths', () => { + it.each([ + ['clip.mp3', 'clip.srt'], + ['clip.part.mp3', 'clip.part.srt'], + ['clip', 'clip.srt'], + ['.clip', '.clip.srt'], + ])('saves subtitles beside %s in a dotted directory', async (filename, subtitleName) => { + const directory = mkdtempSync(join(tmpdir(), 'mmx-subtitles-')); + const outputDirectory = join(directory, 'session.v1'); + mkdirSync(outputDirectory); + const out = join(outputDirectory, filename); + const server = createMockServer({ routes: { + '/v1/t2a_v2': () => jsonResponse({ + base_resp: { status_code: 0, status_msg: 'ok' }, + data: { audio: '48656c6c6f', subtitle_file: `${server.url}/subtitles` }, + }), + '/subtitles': () => jsonResponse([{ text: 'Hello', time_begin: 0, time_end: 1500 }]), + } }); + const output = spyOn(console, 'log').mockImplementation(() => {}); + const flags = { + text: 'Hello', subtitles: true, out, quiet: true, verbose: false, + noColor: true, yes: false, dryRun: false, help: false, nonInteractive: true, async: false, + }; + try { + await synthesizeCommand.execute({ + ...flags, apiKey: 'test-key', region: 'global', baseUrl: server.url, output: 'text', timeout: 10, + }, flags); + expect(readFileSync(out, 'utf8')).toBe('Hello'); + expect(readFileSync(join(outputDirectory, subtitleName), 'utf8')).toBe( + '1\n00:00:00,000 --> 00:00:01,500\nHello', + ); + expect(output).toHaveBeenCalledWith(join(outputDirectory, subtitleName)); + expect(readdirSync(directory)).toEqual(['session.v1']); + } finally { + output.mockRestore(); + server.close(); + rmSync(directory, { recursive: true, force: true }); + } + }); +}); + describe('speech synthesize voice_setting flags', () => { const config = { apiKey: 'test-key',