mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:05:19 +00:00
197 lines
7.2 KiB
TypeScript
197 lines
7.2 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
canRetryRecording,
|
|
canServeRecordingMedia,
|
|
formatRecordingFileSize,
|
|
isRecordingStatus,
|
|
RECORDING_FAILURE_INTERRUPTED,
|
|
RECORDING_MAX_BYTES,
|
|
RECORDING_MAX_SIZE_LABEL,
|
|
RECORDING_STATUSES,
|
|
RECORDING_STEPS,
|
|
recordingFailureMessage,
|
|
recordingFileTypeAllowed,
|
|
recordingStatusLabels,
|
|
recordingTooLargeMessage,
|
|
recordingUploadTooLargeMessage,
|
|
toRecordingViewModel,
|
|
} from '@/lib/recording'
|
|
import {
|
|
buildRecordingAudioFfmpegArgs,
|
|
buildRecordingFullAudioFfmpegArgs,
|
|
mergeChunkTranscriptions,
|
|
RECORDING_AUDIO_CHUNK_SECONDS,
|
|
recordingSearchText,
|
|
} from '@/lib/recordingTranscription'
|
|
|
|
describe('recording vocabulary (C199)', () => {
|
|
it('pins the statuses, steps and their labels', () => {
|
|
expect(RECORDING_STATUSES).toEqual(['uploading', 'processing', 'ready', 'failed'])
|
|
expect(Object.keys(recordingStatusLabels).sort()).toEqual([...RECORDING_STATUSES].sort())
|
|
expect(RECORDING_STEPS).toEqual(['extracting', 'transcribing', 'saving'])
|
|
expect(isRecordingStatus('failed')).toBe(true)
|
|
expect(isRecordingStatus('published')).toBe(false)
|
|
})
|
|
|
|
it('serves media from processing on, never during the upload stream', () => {
|
|
expect(canServeRecordingMedia('uploading')).toBe(false)
|
|
expect(canServeRecordingMedia('processing')).toBe(true)
|
|
expect(canServeRecordingMedia('failed')).toBe(true)
|
|
expect(canServeRecordingMedia('ready')).toBe(true)
|
|
})
|
|
|
|
it('only offers retry for a failed transcription', () => {
|
|
expect(canRetryRecording('failed')).toBe(true)
|
|
expect(canRetryRecording('ready')).toBe(false)
|
|
expect(canRetryRecording('processing')).toBe(false)
|
|
})
|
|
|
|
it('maps the failure cause to honest copy', () => {
|
|
expect(
|
|
recordingFailureMessage({ error: RECORDING_FAILURE_INTERRUPTED, step: 'transcribing' }),
|
|
).toBe(RECORDING_FAILURE_INTERRUPTED)
|
|
expect(recordingFailureMessage({ error: 'raw stderr detail', step: 'extracting' })).toBe(
|
|
'Não foi possível preparar o áudio da gravação.',
|
|
)
|
|
expect(recordingFailureMessage({ error: 'raw stderr detail', step: 'transcribing' })).toBe(
|
|
'Não foi possível transcrever a gravação.',
|
|
)
|
|
expect(recordingFailureMessage({ error: 'raw stderr detail', step: 'saving' })).toBe(
|
|
'Não foi possível guardar a transcrição.',
|
|
)
|
|
expect(recordingFailureMessage({ error: 'raw detail', step: null })).toBe(
|
|
'Não foi possível transcrever a gravação.',
|
|
)
|
|
expect(recordingFailureMessage({ error: null, step: 'transcribing' })).toBeNull()
|
|
expect(recordingFailureMessage({ error: ' ', step: 'transcribing' })).toBeNull()
|
|
})
|
|
|
|
it('accepts videos and unknown types, refuses every other type', () => {
|
|
expect(recordingFileTypeAllowed('video/mp4')).toBe(true)
|
|
expect(recordingFileTypeAllowed('video/quicktime')).toBe(true)
|
|
expect(recordingFileTypeAllowed('')).toBe(true)
|
|
expect(recordingFileTypeAllowed(null)).toBe(true)
|
|
expect(recordingFileTypeAllowed('text/plain')).toBe(false)
|
|
expect(recordingFileTypeAllowed('image/png')).toBe(false)
|
|
})
|
|
|
|
it('formats sizes in pt-BR and names the limit', () => {
|
|
expect(formatRecordingFileSize(4.6 * 1024 ** 3)).toContain('GB')
|
|
expect(formatRecordingFileSize(812 * 1024 ** 2)).toBe('812 MB')
|
|
expect(recordingTooLargeMessage(4.6 * 1024 ** 3)).toContain('4,6 GB')
|
|
expect(recordingTooLargeMessage(4.6 * 1024 ** 3)).toContain(RECORDING_MAX_SIZE_LABEL)
|
|
expect(recordingUploadTooLargeMessage).toContain(RECORDING_MAX_SIZE_LABEL)
|
|
expect(RECORDING_MAX_BYTES).toBe(4 * 1024 ** 3)
|
|
})
|
|
|
|
it('formats the byte range below 1 KB honestly', () => {
|
|
expect(formatRecordingFileSize(0)).toBe('0 B')
|
|
expect(formatRecordingFileSize(900)).toBe('900 B')
|
|
expect(formatRecordingFileSize(2048)).toBe('2 KB')
|
|
})
|
|
|
|
it('builds the view model with labels and hrefs', () => {
|
|
const view = toRecordingViewModel({
|
|
id: 7,
|
|
title: ' Plenária da Comissão ',
|
|
status: 'ready',
|
|
step: null,
|
|
recordedAt: '2026-09-01T00:00:00.000Z',
|
|
durationSeconds: 3725,
|
|
})
|
|
expect(view.title).toBe('Plenária da Comissão')
|
|
expect(view.recordedAtLabel).toBe('01/09/2026')
|
|
expect(view.durationLabel).toBe('1:02:05')
|
|
expect(view.detailHref).toBe('/campanha/comunicacao/acervo/gravacoes/7')
|
|
expect(view.fileHref).toBe('/campanha/comunicacao/acervo/gravacoes/7/arquivo')
|
|
expect(view.downloadHref).toBe('/campanha/comunicacao/acervo/gravacoes/7/arquivo?download=1')
|
|
expect(view.retryHref).toBe('/campanha/comunicacao/acervo/gravacoes/7/retry')
|
|
expect(view.deleteHref).toBe('/campanha/comunicacao/acervo/gravacoes/7/apagar')
|
|
|
|
const degraded = toRecordingViewModel({
|
|
id: 8,
|
|
title: '',
|
|
status: 'stray',
|
|
step: 'stray',
|
|
recordedAt: null,
|
|
durationSeconds: null,
|
|
})
|
|
expect(degraded.title).toBe('Gravação 8')
|
|
expect(degraded.status).toBe('failed')
|
|
expect(degraded.step).toBeNull()
|
|
expect(degraded.recordedAtLabel).toBeNull()
|
|
expect(degraded.durationLabel).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('recording transcription (C199)', () => {
|
|
it('builds the extract-and-split ffmpeg command', () => {
|
|
const args = buildRecordingAudioFfmpegArgs({
|
|
inputPath: '/tmp/in.mkv',
|
|
outputPattern: '/tmp/chunk-%03d.mp3',
|
|
})
|
|
expect(args).toContain('-vn')
|
|
expect(args).toContain('libmp3lame')
|
|
expect(args[args.indexOf('-segment_time') + 1]).toBe(String(RECORDING_AUDIO_CHUNK_SECONDS))
|
|
expect(args.at(-1)).toBe('/tmp/chunk-%03d.mp3')
|
|
})
|
|
|
|
it('merges chunks with absolute offsets, contiguous order and clamped spans', () => {
|
|
const merged = mergeChunkTranscriptions([
|
|
{
|
|
offsetSeconds: 0,
|
|
segments: [
|
|
{ start: 1, end: 4, text: 'primeiro trecho' },
|
|
{ start: 4, end: 2, text: 'clamp aqui' },
|
|
{ start: 5, end: 6, text: ' ' },
|
|
],
|
|
},
|
|
{
|
|
offsetSeconds: 1200,
|
|
segments: [{ start: 0.5, end: 3, text: 'segundo chunk' }],
|
|
},
|
|
])
|
|
|
|
expect(merged).toHaveLength(3)
|
|
expect(merged[0]).toEqual({
|
|
order: 0,
|
|
startSeconds: 1,
|
|
endSeconds: 4,
|
|
text: 'primeiro trecho',
|
|
})
|
|
expect(merged[1]).toEqual({ order: 1, startSeconds: 4, endSeconds: 4, text: 'clamp aqui' })
|
|
expect(merged[2]).toMatchObject({
|
|
order: 2,
|
|
startSeconds: 1200.5,
|
|
endSeconds: 1203,
|
|
text: 'segundo chunk',
|
|
})
|
|
})
|
|
|
|
it('normalizes the concatenated search text', () => {
|
|
expect(recordingSearchText([{ text: 'Ação da Comissão' }, { text: ' de EDUCAÇÃO ' }])).toBe(
|
|
'acao da comissao de educacao',
|
|
)
|
|
})
|
|
|
|
it('returns nothing for an empty transcription', () => {
|
|
expect(mergeChunkTranscriptions([])).toEqual([])
|
|
expect(recordingSearchText([])).toBe('')
|
|
})
|
|
|
|
it('builds the single-file extraction for diarization (C200)', () => {
|
|
const args = buildRecordingFullAudioFfmpegArgs({
|
|
inputPath: '/tmp/in.mkv',
|
|
outputPath: '/tmp/full.mp3',
|
|
})
|
|
expect(args).toContain('-vn')
|
|
expect(args).toContain('libmp3lame')
|
|
// One output, no segmentation: speaker numbering needs the whole audio.
|
|
expect(args).not.toContain('-f')
|
|
expect(args).not.toContain('-segment_time')
|
|
expect(args.at(-1)).toBe('/tmp/full.mp3')
|
|
})
|
|
})
|