feat(core): provider check helpers — pick latest audio, format notice
Refs #5 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
This commit is contained in:
parent
87826bc427
commit
129e7c0b40
@ -24,6 +24,13 @@ export function extFromMime(mime: string): string {
|
||||
return EXT[base] ?? 'bin';
|
||||
}
|
||||
|
||||
// Обратное к extFromMime; .bin и неизвестные расширения → null.
|
||||
export function mimeFromExt(ext: string): string | null {
|
||||
const lower = ext.toLowerCase();
|
||||
for (const [mime, e] of Object.entries(EXT)) if (e === lower) return mime;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function audioPath(id: string, mime: string): string {
|
||||
return `${SOURCE_DIR}/${id}.${extFromMime(mime)}`;
|
||||
}
|
||||
|
||||
36
src/core/provider-check.ts
Normal file
36
src/core/provider-check.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { mimeFromExt, SOURCE_DIR } from './naming';
|
||||
import type { TranscribeResult } from './ports';
|
||||
|
||||
// Команда «Проверить провайдер распознавания»: только уведомление,
|
||||
// ни заметки, ни очередь не трогаются.
|
||||
|
||||
export const NO_CHECK_AUDIO = `Нет записей в ${SOURCE_DIR}/ для проверки провайдера`;
|
||||
|
||||
const PREVIEW_LIMIT = 100;
|
||||
|
||||
// Последний по имени файл прямо в voice/source/: имена начинаются с UTC-времени,
|
||||
// значит это самая свежая запись.
|
||||
export function pickCheckAudio(paths: readonly string[]): { path: string; mime: string } | null {
|
||||
const prefix = `${SOURCE_DIR}/`;
|
||||
let best: { path: string; mime: string } | null = null;
|
||||
for (const path of paths) {
|
||||
if (!path.startsWith(prefix)) continue;
|
||||
const name = path.slice(prefix.length);
|
||||
const dot = name.lastIndexOf('.');
|
||||
if (name.includes('/') || dot < 0) continue;
|
||||
const mime = mimeFromExt(name.slice(dot + 1));
|
||||
if (mime && (!best || path > best.path)) best = { path, mime };
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export function checkNotice(result: TranscribeResult): string {
|
||||
if (result.ok) {
|
||||
const text = result.text.trim();
|
||||
if (!text) return 'Провайдер работает, но речь в записи не распознана';
|
||||
const preview = text.length > PREVIEW_LIMIT ? `${text.slice(0, PREVIEW_LIMIT)}…` : text;
|
||||
return `Провайдер работает: «${preview}»`;
|
||||
}
|
||||
const kind = result.kind === 'temporary' ? 'временная' : 'постоянная';
|
||||
return `Провайдер: ${kind} ошибка — ${result.message}`;
|
||||
}
|
||||
@ -5,6 +5,7 @@ import {
|
||||
formatDuration,
|
||||
formatLocalTime,
|
||||
makeId,
|
||||
mimeFromExt,
|
||||
notePath,
|
||||
} from '../src/core/naming';
|
||||
|
||||
@ -77,3 +78,18 @@ describe('formatting', () => {
|
||||
expect(formatLocalTime('2026-09-27T11:17:03.000Z')).toBe('14:17');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mimeFromExt', () => {
|
||||
it.each([
|
||||
['m4a', 'audio/mp4'],
|
||||
['M4A', 'audio/mp4'],
|
||||
['webm', 'audio/webm'],
|
||||
['ogg', 'audio/ogg'],
|
||||
])('%s → %s', (ext, mime) => {
|
||||
expect(mimeFromExt(ext)).toBe(mime);
|
||||
});
|
||||
|
||||
it.each(['bin', 'mp3', '', 'md'])('%j → null', (ext) => {
|
||||
expect(mimeFromExt(ext)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
56
tests/provider-check.test.ts
Normal file
56
tests/provider-check.test.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { checkNotice, pickCheckAudio } from '../src/core/provider-check';
|
||||
|
||||
describe('pickCheckAudio', () => {
|
||||
it('picks the last audio file by name directly in voice/source/', () => {
|
||||
expect(
|
||||
pickCheckAudio([
|
||||
'voice/source/20260927-111503-0000.m4a',
|
||||
'voice/source/20260927-120000-abcd.webm',
|
||||
'voice/source/20260926-235959-ffff.ogg',
|
||||
'voice/inbox/2026-09-27 15-00-00.md',
|
||||
]),
|
||||
).toEqual({ path: 'voice/source/20260927-120000-abcd.webm', mime: 'audio/webm' });
|
||||
});
|
||||
|
||||
it('ignores .bin, unknown extensions, subfolders and look-alike folders', () => {
|
||||
expect(
|
||||
pickCheckAudio([
|
||||
'voice/source/20260927-111503-0000.m4a',
|
||||
'voice/source/20260928-000000-0000.bin',
|
||||
'voice/source/20260928-000000-0001.mp3',
|
||||
'voice/source/old/20260929-000000-0000.m4a',
|
||||
'voice/sources/20260929-000000-0000.m4a',
|
||||
'voice/source/noext',
|
||||
]),
|
||||
).toEqual({ path: 'voice/source/20260927-111503-0000.m4a', mime: 'audio/mp4' });
|
||||
});
|
||||
|
||||
it('returns null when nothing fits', () => {
|
||||
expect(pickCheckAudio([])).toBeNull();
|
||||
expect(pickCheckAudio(['voice/source/a.bin', 'notes/x.m4a'])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkNotice', () => {
|
||||
it('shows the start of the text', () => {
|
||||
expect(checkNotice({ ok: true, text: ' Привет, мир ' })).toBe('Провайдер работает: «Привет, мир»');
|
||||
});
|
||||
|
||||
it('cuts long text to 100 chars with an ellipsis', () => {
|
||||
expect(checkNotice({ ok: true, text: 'а'.repeat(150) })).toBe(`Провайдер работает: «${'а'.repeat(100)}…»`);
|
||||
});
|
||||
|
||||
it('reports no speech for empty text', () => {
|
||||
expect(checkNotice({ ok: true, text: ' ' })).toBe('Провайдер работает, но речь в записи не распознана');
|
||||
});
|
||||
|
||||
it('shows kind and reason of an error', () => {
|
||||
expect(checkNotice({ ok: false, kind: 'temporary', message: 'HTTP 429' })).toBe(
|
||||
'Провайдер: временная ошибка — HTTP 429',
|
||||
);
|
||||
expect(checkNotice({ ok: false, kind: 'permanent', message: 'HTTP 401: Invalid API key' })).toBe(
|
||||
'Провайдер: постоянная ошибка — HTTP 401: Invalid API key',
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user