Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/commands/speech/synthesize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion test/commands/speech/synthesize.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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',
Expand Down