anc
Проверка звука до записи и предупреждение, если дорожка молчит
Встреча, которая молча не записалась, — худшее для программы: человек
узнаёт об этом после встречи.
- «Новая запись»: проверка звука — микрофон и звук компьютера открываются
без записи в файл (audio_check_start/stop), две живые полоски
«слышно / тишина». Устройство не открылось — причина и где разрешить
(на Mac — «Запись экрана и системного звука»).
- Во время записи: дорожка минуту тише −60 dBFS — событие silent,
сообщение и строка на панели записи; звук вернулся — heard, строка
уходит. На паузе тишина не считается; звук компьютера, который в
тишине не приходит вовсе, тоже ловится.
- Начало записи гасит проверку: устройства не заняты дважды.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
57e443b
1 октября 2026 г.
new.ts
// Экран «Новая запись»: «Записать встречу» ведёт сюда, а не запускает запись
// сразу (решение владельца 01.10.2026). Здесь проверка звука — две живые
// полоски, микрофон и звук компьютера, — и совет про наушники; запись
// начинается только кнопкой «Начать запись».
//
// Проверка — главная защита от встречи, которая молча не записалась: что
// звука собеседников нет, человек видит до встречи, а не после.
import { api, events, type CheckView } from "../api";
import { esc } from "../dom";
import type { Scope } from "../scope";
import { startRecording, state } from "../state";
export async function renderNew(root: HTMLElement, scope: Scope): Promise {
// Запись уже идёт — вторую не начать, показываем идущую.
if (state.recording) {
location.replace(`#/m/${encodeURIComponent(state.recording.meeting_id)}`);
return;
}
const view = await api.getSettings();
if (!scope.alive) return;
if (!view.capture_supported) {
root.innerHTML = `
Новая запись
На этой системе запись не поддерживается. Добавьте готовый аудиофайл — перетащите его в окно или выберите через меню.
`;
return;
}
root.innerHTML = `
Новая запись
Проверка звука
${checkRow("me", "Микрофон", "скажите что-нибудь")}
${checkRow("others", "Звук компьютера", "включите любой звук — видео, музыку; на встрече это голоса собеседников")}
Лучше в наушниках — так голоса собеседников не попадут в микрофон второй раз.
${
view.openrouter_key
? ""
: `Записать можно и без ИИ. Чтобы потом расшифровать, подключите его в настройках.`
}
Начать запись
`;
// Проверка звука: уровни приходят, пока открыт экран; уходим — устройства
// закрываются. «Начать запись» ядро гасит проверку само.
let check: CheckView | null = null;
try {
check = await api.audioCheckStart();
} catch (e) {
check = { mic_error: String(e), system_error: String(e) };
}
if (!scope.alive) {
void api.audioCheckStop();
return;
}
showError(root, "me", check.mic_error);
showError(root, "others", check.system_error);
const heard: Record = {};
scope.on(
events.recorder(({ meeting_id, event }) => {
if (meeting_id || event.kind !== "level") return;
const key = event.data.speaker.kind === "me" ? "me" : "others";
const row = root.querySelector(`[data-check-row="${key}"]`);
if (!row || row.dataset.error) return;
const fill = row.querySelector(".meter-fill");
if (fill) fill.style.width = `${Math.max(0, Math.min(100, ((event.data.dbfs + 60) / 55) * 100))}%`;
if (event.data.dbfs > -45) heard[key] = performance.now();
const ok = performance.now() - (heard[key] ?? -1e9) < 3_000;
row.classList.toggle("is-ok", ok);
row.querySelector("[data-check-state]")!.textContent = ok ? "слышно" : "тишина";
}),
);
scope.on(Promise.resolve(() => void api.audioCheckStop()));
root.querySelector("[data-start]")!.addEventListener("click", async (e) => {
const b = e.currentTarget as HTMLButtonElement;
b.disabled = true;
b.textContent = "Начинаю…";
await startRecording();
// Не началась — ошибку уже показало сообщение внизу, кнопку возвращаем.
if (scope.alive && !state.recording) {
b.disabled = false;
b.textContent = "Начать запись";
}
});
}
function checkRow(key: "me" | "others", title: string, hint: string): string {
return `
${title}
тишина
${esc(hint)}
`;
}
/** Устройство не открылось — вместо полоски причина и где разрешить. */
function showError(root: HTMLElement, key: "me" | "others", error: string | null): void {
if (!error) return;
const row = root.querySelector(`[data-check-row="${key}"]`);
if (!row) return;
row.dataset.error = "1";
row.classList.add("is-error");
row.querySelector("[data-check-state]")!.textContent = "не пишется";
const where =
key === "others"
? " На Mac: Системные настройки → Конфиденциальность и безопасность → «Запись экрана и системного звука» → включите Skillok Meet и перезапустите программу."
: " Проверьте, что микрофон подключён и программе разрешён доступ к нему.";
row.querySelector("[data-check-hint]")!.textContent = error + where;
}