// ==UserScript==
// @name AutoNektome
// @namespace http://tampermonkey.net/
// @version 4.1.1
// @description Автоматический переход с настройками звука, голосовым управлением, улучшенной автогромкостью, изменением голоса и выбором тем для nekto.me audiochat
// @author @paracosm17
// @match https://nekto.me/audiochat
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ### Настройка звуков уведомлений
const START_CONVERSATION_SOUND_URL = 'https://zvukogram.com/mp3/p2/2862/skayp-zvuk-soobschenie-poluchil-message-received-23007.mp3'; // Ссылка на звук начала разговора
const END_CONVERSATION_SOUND_URL = 'https://zvukogram.com//mp3/cats/791/enderman_teleport.mp3'; // Ссылка на звук окончания разговора
const START_SOUND_VOLUME = 0.4; // Громкость звука начала разговора (0.0 - 1.0)
const END_SOUND_VOLUME = 0.3; // Громкость звука окончания разговора (0.0 - 1.0)
// ### Настройка голосовых команд
const VOICE_COMMANDS = {
skip: ['скип', 'skip', 'скиф', 'скипнуть', 'кефир'],
stop: ['завершить', 'остановить', 'закончить', 'кумыс'],
start: ['чат']
};
// ### Настройки автогромкости собеседника
const TARGET_VOLUME = 50; // Целевая громкость звука в процентах (0-100), к которой стремится автогромкость
const MIN_VOLUME = 10; // Минимально допустимая громкость в процентах (0-100), ниже которой автогромкость не опустит звук
const MAX_VOLUME = 90; // Максимально допустимая громкость в процентах (0-100), выше которой автогромкость не поднимет звук
const TRANSITION_DURATION = 1000; // Длительность плавного перехода громкости в миллисекундах (1000 мс = 1 секунда)
const VOLUME_CHECK_INTERVAL = 200; // Интервал проверки громкости в миллисекундах (как часто анализируется уровень звука)
const HOLD_DURATION = 5000; // Время удержания текущей громкости в миллисекундах после громкого звука (5000 мс = 5 секунд)
const SILENCE_THRESHOLD = 5; // Порог тишины в процентах (0-100), ниже которого звук считается слишком тихим
const HISTORY_SIZE = 15; // Размер истории измерений громкости (количество последних значений для усреднения)
// ### Темы
const THEMES = {
'Original': null,
'Dracula': 'https://raw.githubusercontent.com/paracosm17/AutoNektome/refs/heads/main/dracula.css',
'GitHub Dark': 'https://raw.githubusercontent.com/paracosm17/AutoNektome/refs/heads/main/githubdark.css',
'One Dark': 'https://raw.githubusercontent.com/paracosm17/AutoNektome/refs/heads/main/onedark.css',
'Monokai': 'https://raw.githubusercontent.com/paracosm17/AutoNektome/refs/heads/main/monokai.css',
'Nord': 'https://raw.githubusercontent.com/paracosm17/AutoNektome/refs/heads/main/nord.css'
};
// ### Настройки из localStorage
const settings = {
enableLoopback: loadSetting('enableLoopback', false),
autoGainControl: loadSetting('autoGainControl', false),
noiseSuppression: loadSetting('noiseSuppression', true),
echoCancellation: loadSetting('echoCancellation', false),
gainValue: loadSetting('gainValue', 1.5, parseFloat),
voiceControl: loadSetting('voiceControl', false),
autoVolume: loadSetting('autoVolume', true),
voicePitch: loadSetting('voicePitch', false),
pitchLevel: loadSetting('pitchLevel', 0, parseFloat),
conversationCount: loadSetting('conversationCount', 0, parseInt),
conversationStats: loadSetting('conversationStats', {
over5min: 0,
over15min: 0,
over30min: 0,
over1hour: 0,
over2hours: 0,
over3hours: 0,
over5hours: 0
}),
selectedTheme: loadSetting('selectedTheme', 'Original')
};
// ### Переменные состояния
let isAutoModeEnabled = true;
let isVoiceControlEnabled = settings.voiceControl;
let observer = null;
let globalStream = null;
let audioContext = null;
let gainNode = null;
let micStream = null;
let recognition = null;
let voiceHintElement = null;
let remoteAudioContext = null;
let volumeAnalyser = null;
let volumeCheckIntervalId = null;
let lastLoudTime = 0;
let volumeHistory = [];
let lastAdjustedVolume = TARGET_VOLUME;
let pitchNode = null;
let pitchAudioContext = null;
let pitchSource = null;
let pitchWorkletNode = null;
let conversationTimer = null;
let currentConversationStart = null;
let isConversationActive = false;
let isMicMuted = false;
let isHeadphonesMuted = false;
let currentThemeLink = null;
// ### Утилиты
const endConversationAudio = new Audio(END_CONVERSATION_SOUND_URL);
endConversationAudio.volume = END_SOUND_VOLUME;
const startConversationAudio = new Audio(START_CONVERSATION_SOUND_URL);
startConversationAudio.volume = START_SOUND_VOLUME;
// Блокировка звука connect.mp3 через MutationObserver
const blockConnectSound = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
const audioElements = document.querySelectorAll('audio');
audioElements.forEach(audio => {
if (audio.src.includes('connect.mp3') && !audio.dataset.custom) {
audio.src = '';
audio.muted = true;
audio.pause();
audio.removeAttribute('preload');
audio.setAttribute('data-blocked', 'true');
console.log('Звук connect.mp3 заблокирован');
}
});
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
};
// Запускаем блокировку сразу
blockConnectSound();
const originalPlay = HTMLAudioElement.prototype.play;
HTMLAudioElement.prototype.play = function() {
if (this.src.includes('connect.mp3') && !this.dataset.custom) {
console.log('Попытка воспроизведения connect.mp3 заблокирована');
return Promise.resolve();
}
return originalPlay.apply(this, arguments);
};
function loadSetting(key, defaultValue, transform = JSON.parse) {
const value = localStorage.getItem(key);
return value !== null ? transform(value) : defaultValue;
}
function saveSetting(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
// ### Управление темами
function applyTheme(themeName) {
if (currentThemeLink) {
currentThemeLink.remove();
currentThemeLink = null;
}
const loadingIndicator = document.querySelector('#settings-container select + span + span');
if (loadingIndicator) loadingIndicator.style.display = 'block';
if (themeName !== 'Original' && THEMES[themeName]) {
const styleElement = document.createElement('style');
styleElement.id = 'custom-theme-style';
fetch(THEMES[themeName])
.then(response => {
if (!response.ok) throw new Error('Ошибка загрузки CSS');
return response.text();
})
.then(css => {
styleElement.textContent = css;
document.head.appendChild(styleElement);
currentThemeLink = styleElement;
if (loadingIndicator) loadingIndicator.style.display = 'none';
})
.catch(error => {
console.error('Ошибка при загрузке темы:', error);
if (loadingIndicator) loadingIndicator.style.display = 'none';
});
} else if (themeName === 'Original') {
const existingStyles = document.querySelectorAll('style[id="custom-theme-style"]');
existingStyles.forEach(style => style.remove());
currentThemeLink = null;
if (loadingIndicator) loadingIndicator.style.display = 'none';
}
settings.selectedTheme = themeName;
saveSetting('selectedTheme', themeName);
}
function createThemeSelector() {
const themeContainer = document.createElement('div');
themeContainer.style.marginTop = '20px';
const themeLabel = document.createElement('span');
themeLabel.textContent = 'Тема оформления';
themeLabel.style.fontSize = '14px';
themeLabel.style.color = '#fff';
themeLabel.style.fontWeight = 'bold';
themeLabel.style.textShadow = '0 0 3px rgba(255,255,255,0.5)';
themeLabel.style.display = 'block';
themeLabel.style.marginBottom = '8px';
const selectWrapper = document.createElement('div');
selectWrapper.style.position = 'relative';
selectWrapper.style.width = '100%';
const select = document.createElement('select');
select.style.width = '100%';
select.style.padding = '8px 25px 8px 10px';
select.style.background = '#2b2b2b';
select.style.color = '#fff';
select.style.border = '1px solid #ff007a';
select.style.borderRadius = '8px';
select.style.fontSize = '14px';
select.style.cursor = 'pointer';
select.style.appearance = 'none';
select.style.outline = 'none';
select.style.transition = 'border-color 0.3s ease';
for (const themeName in THEMES) {
const option = document.createElement('option');
option.value = themeName;
option.textContent = themeName;
if (themeName === settings.selectedTheme) {
option.selected = true;
}
select.appendChild(option);
}
const arrow = document.createElement('span');
arrow.textContent = '▼';
arrow.style.position = 'absolute';
arrow.style.right = '10px';
arrow.style.top = '50%';
arrow.style.transform = 'translateY(-50%)';
arrow.style.color = '#ff007a';
arrow.style.pointerEvents = 'none';
selectWrapper.appendChild(select);
selectWrapper.appendChild(arrow);
themeContainer.appendChild(themeLabel);
themeContainer.appendChild(selectWrapper);
select.addEventListener('change', (e) => {
const selectedTheme = e.target.value;
applyTheme(selectedTheme);
});
select.addEventListener('mouseover', () => {
select.style.borderColor = '#00ff9d';
});
select.addEventListener('mouseout', () => {
select.style.borderColor = '#ff007a';
});
return themeContainer;
}
// ### AudioWorklet процессор для pitch shifting
const pitchShiftWorkletCode = `
class PitchShiftProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.bufferSize = 4096;
this.buffer = new Float32Array(this.bufferSize);
this.writeIndex = 0;
this.readIndex = 0;
this.pitchFactor = 1.0;
this.port.onmessage = (event) => {
this.pitchFactor = event.data;
};
}
process(inputs, outputs, parameters) {
const input = inputs[0][0];
const output = outputs[0][0];
if (!input || !output) return true;
for (let i = 0; i < input.length; i++) {
this.buffer[this.writeIndex] = input[i];
this.writeIndex = (this.writeIndex + 1) % this.bufferSize;
}
for (let i = 0; i < output.length; i++) {
const intIndex = Math.floor(this.readIndex);
const frac = this.readIndex - intIndex;
const sample1 = this.buffer[intIndex % this.bufferSize];
const sample2 = this.buffer[(intIndex + 1) % this.bufferSize];
output[i] = sample1 + (sample2 - sample1) * frac;
this.readIndex = (this.readIndex + this.pitchFactor) % this.bufferSize;
}
return true;
}
}
registerProcessor('pitch-shift-processor', PitchShiftProcessor);
`;
// ### Функции авторежима
function checkAndClickButton() {
if (!isAutoModeEnabled) return;
const button = document.querySelector('button.btn.btn-lg.go-scan-button');
if (button) {
button.click();
}
}
function skipConversation() {
const stopButton = document.querySelector('button.btn.btn-lg.stop-talk-button');
if (stopButton) {
stopButton.click();
setTimeout(() => {
const confirmButton = document.querySelector('button.swal2-confirm.swal2-styled');
if (confirmButton) {
confirmButton.click();
playNotificationOnEnd();
}
}, 500);
}
}
function playNotificationOnEnd() {
if (isConversationActive) {
endConversationAudio.play();
isConversationActive = false;
}
}
function playNotificationOnStart() {
if (!isConversationActive) {
startConversationAudio.dataset.custom = 'true'; // Помечаем как кастомный звук
startConversationAudio.play();
isConversationActive = true;
}
}
function updateSliderStyles(enable) {
const slider = document.querySelector('.slider');
const sliderCircle = document.querySelector('.slider-circle');
if (slider) slider.style.background = enable ? '#00ff9d' : '#555';
if (sliderCircle) sliderCircle.style.left = enable ? '40px' : '4px';
}
function applyCustomStyles(enable) {
const styleId = 'custom-slider-styles';
let styleElement = document.getElementById(styleId);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
styleElement.textContent = `
.slider { background: ${enable ? '#00ff9d' : '#555'} !important; transition: background 0.4s !important; }
.slider-circle { left: ${enable ? '40px' : '4px'} !important; }
`;
}
function toggleAutoMode(enable) {
isAutoModeEnabled = enable;
const toggleInput = document.querySelector('input[type="checkbox"]');
const toggleLabel = document.querySelector('span.toggle-label');
if (toggleInput) toggleInput.checked = enable;
if (toggleLabel) {
toggleLabel.textContent = `Авторежим ${enable ? 'ВКЛ' : 'ВЫКЛ'}`;
toggleLabel.style.color = enable ? '#00ff9d' : '#ff4d4d';
toggleLabel.style.textShadow = `0 0 5px ${enable ? '#00ff9d' : '#ff4d4d'}`;
}
updateSliderStyles(enable);
applyCustomStyles(enable);
}
// ### Аудио функции
async function getMicStream() {
try {
micStream = await navigator.mediaDevices.getUserMedia({
audio: {
autoGainControl: settings.autoGainControl,
noiseSuppression: settings.noiseSuppression,
echoCancellation: settings.echoCancellation
}
});
return micStream;
} catch (e) {
console.error('Ошибка получения микрофона:', e);
return null;
}
}
function enableSelfListening(stream) {
if (!stream || !stream.getAudioTracks().length) return;
if (audioContext) audioContext.close();
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
gainNode = audioContext.createGain();
gainNode.gain.value = settings.gainValue;
if (settings.voicePitch && settings.pitchLevel > 0) {
pitchNode = audioContext.createBiquadFilter();
pitchNode.type = 'lowshelf';
pitchNode.frequency.value = 300;
pitchNode.gain.value = 10;
source.connect(pitchNode);
pitchNode.connect(gainNode);
} else {
source.connect(gainNode);
}
gainNode.connect(audioContext.destination);
}
async function createPitchShiftedStream(stream) {
if (pitchAudioContext) pitchAudioContext.close();
pitchAudioContext = new AudioContext();
pitchSource = pitchAudioContext.createMediaStreamSource(stream);
const outputNode = pitchAudioContext.createGain();
if (settings.voicePitch && settings.pitchLevel > 0) {
const blob = new Blob([pitchShiftWorkletCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
await pitchAudioContext.audioWorklet.addModule(url);
pitchWorkletNode = new AudioWorkletNode(pitchAudioContext, 'pitch-shift-processor');
const pitchShiftFactor = 1.0 - settings.pitchLevel;
pitchWorkletNode.port.postMessage(pitchShiftFactor);
pitchNode = pitchAudioContext.createBiquadFilter();
pitchNode.type = 'lowshelf';
pitchNode.frequency.value = 300;
pitchNode.gain.value = 10;
pitchSource.connect(pitchWorkletNode);
pitchWorkletNode.connect(pitchNode);
pitchNode.connect(outputNode);
} else {
pitchSource.connect(outputNode);
}
const destination = pitchAudioContext.createMediaStreamDestination();
outputNode.connect(destination);
return destination.stream;
}
function updatePitchEffect(enable) {
settings.voicePitch = enable;
if (globalStream) {
createPitchShiftedStream(micStream || globalStream).then(newStream => {
globalStream.getAudioTracks().forEach(track => track.stop());
globalStream = newStream;
if (settings.enableLoopback) enableSelfListening(newStream);
});
}
}
function updatePitchLevel(value) {
settings.pitchLevel = value;
localStorage.setItem('pitchLevel', value);
if (pitchWorkletNode) {
const pitchShiftFactor = 1.0 - settings.pitchLevel;
pitchWorkletNode.port.postMessage(pitchShiftFactor);
} else if (globalStream) {
createPitchShiftedStream(micStream || globalStream).then(newStream => {
globalStream.getAudioTracks().forEach(track => track.stop());
globalStream = newStream;
if (settings.enableLoopback) enableSelfListening(newStream);
});
}
}
// ### Улучшенная автогромкость
function setupAutoVolume(stream) {
if (!settings.autoVolume || !stream) return;
if (remoteAudioContext) remoteAudioContext.close();
if (volumeCheckIntervalId) clearInterval(volumeCheckIntervalId);
remoteAudioContext = new AudioContext();
const source = remoteAudioContext.createMediaStreamSource(stream);
volumeAnalyser = remoteAudioContext.createAnalyser();
volumeAnalyser.fftSize = 256;
source.connect(volumeAnalyser);
const bufferLength = volumeAnalyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const audioElement = document.querySelector('audio#audioStream');
volumeHistory = [];
lastAdjustedVolume = TARGET_VOLUME;
function adjustVolume() {
if (!settings.autoVolume || !volumeAnalyser || !audioElement) return;
volumeAnalyser.getByteTimeDomainData(dataArray);
let sum = 0;
for (let i = 0; i < bufferLength; i++) {
const value = (dataArray[i] - 128) / 128;
sum += value * value;
}
const rms = Math.sqrt(sum / bufferLength);
const volumeLevel = Math.min(1, rms * 10) * 100;
volumeHistory.push(volumeLevel);
if (volumeHistory.length > HISTORY_SIZE) volumeHistory.shift();
const avgVolume = volumeHistory.reduce((a, b) => a + b, 0) / volumeHistory.length;
const volumeSlider = document.querySelector('.volume_slider input.slider-input');
if (!volumeSlider) return;
let targetValue;
const currentTime = Date.now();
if (avgVolume > TARGET_VOLUME + 20) {
targetValue = Math.max(MIN_VOLUME, TARGET_VOLUME - (avgVolume - TARGET_VOLUME));
lastLoudTime = currentTime;
lastAdjustedVolume = targetValue;
} else if (currentTime - lastLoudTime < HOLD_DURATION || avgVolume < SILENCE_THRESHOLD) {
targetValue = lastAdjustedVolume;
} else if (avgVolume < TARGET_VOLUME - 20) {
targetValue = Math.min(MAX_VOLUME, lastAdjustedVolume + (TARGET_VOLUME - avgVolume) / 2);
lastAdjustedVolume = targetValue;
} else {
targetValue = lastAdjustedVolume;
}
const startValue = parseInt(volumeSlider.value) || TARGET_VOLUME;
if (Math.abs(startValue - targetValue) > 5) {
smoothTransition(volumeSlider, startValue, targetValue, audioElement);
}
}
function smoothTransition(slider, startValue, targetValue, audio) {
const startTime = performance.now();
function step(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / TRANSITION_DURATION, 1);
const newValue = startValue + (targetValue - startValue) * progress;
slider.value = newValue;
slider.dispatchEvent(new Event('input', { bubbles: true }));
slider.dispatchEvent(new Event('change', { bubbles: true }));
updateSliderVisuals(newValue);
audio.volume = newValue / 100;
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
function updateSliderVisuals(value) {
const sliderDot = document.querySelector('.slider-dot');
const sliderProcess = document.querySelector('.slider-process');
const tooltip = document.querySelector('.slider-tooltip');
if (sliderDot && sliderProcess && tooltip) {
const maxTranslate = 48;
const translateX = (value / 100) * maxTranslate;
sliderDot.style.transform = `translateX(${translateX}px)`;
sliderProcess.style.width = `${translateX + 4}px`;
tooltip.textContent = Math.round(value);
}
}
volumeCheckIntervalId = setInterval(adjustVolume, VOLUME_CHECK_INTERVAL);
}
// ### Голосовое управление
async function initSpeechRecognition() {
if (!('webkitSpeechRecognition' in window)) return;
if (!micStream) await getMicStream();
recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = false;
recognition.lang = 'ru-RU';
recognition.onresult = (event) => {
if (!isVoiceControlEnabled) return;
const transcript = event.results[event.results.length - 1][0].transcript.trim().toLowerCase();
if (VOICE_COMMANDS.skip.some(cmd => transcript.includes(cmd))) {
skipConversation();
} else if (VOICE_COMMANDS.stop.some(cmd => transcript.includes(cmd))) {
toggleAutoMode(false);
skipConversation();
} else if (VOICE_COMMANDS.start.some(cmd => transcript.includes(cmd))) {
toggleAutoMode(true);
checkAndClickButton();
}
};
recognition.onerror = (event) => {
if (event.error !== 'aborted' && isVoiceControlEnabled) setTimeout(() => recognition.start(), 100);
};
recognition.onend = () => {
if (isVoiceControlEnabled) setTimeout(() => recognition.start(), 100);
};
if (settings.voiceControl) {
isVoiceControlEnabled = true;
recognition.start();
if (voiceHintElement) voiceHintElement.style.display = 'inline-block';
setInterval(() => {
if (isVoiceControlEnabled) {
recognition.stop();
setTimeout(() => recognition.start(), 200);
}
}, 10000);
}
}
// ### Счетчик разговоров
function updateConversationStats(duration) {
settings.conversationCount++;
if (duration >= 300) settings.conversationStats.over5min++;
if (duration >= 900) settings.conversationStats.over15min++;
if (duration >= 1800) settings.conversationStats.over30min++;
if (duration >= 3600) settings.conversationStats.over1hour++;
if (duration >= 7200) settings.conversationStats.over2hours++;
if (duration >= 10800) settings.conversationStats.over3hours++;
if (duration >= 18000) settings.conversationStats.over5hours++;
saveSetting('conversationCount', settings.conversationCount);
saveSetting('conversationStats', settings.conversationStats);
const counter = document.querySelector('#conversation-counter span');
if (counter) counter.textContent = `Разговоров: ${settings.conversationCount}`;
}
function startConversationTimer() {
if (conversationTimer) clearInterval(conversationTimer);
currentConversationStart = Date.now();
playNotificationOnStart();
conversationTimer = setInterval(() => {
const timerElement = document.querySelector('.timer-label');
if (!timerElement || timerElement.textContent === '00:00') {
stopConversationTimer();
}
}, 1000);
}
function stopConversationTimer() {
if (conversationTimer && currentConversationStart) {
clearInterval(conversationTimer);
const duration = Math.floor((Date.now() - currentConversationStart) / 1000);
updateConversationStats(duration);
playNotificationOnEnd();
conversationTimer = null;
currentConversationStart = null;
}
}
// ### Новые функции для управления микрофоном и наушниками
function toggleMic() {
isMicMuted = !isMicMuted;
if (globalStream) {
globalStream.getAudioTracks().forEach(track => {
track.enabled = !isMicMuted;
});
}
updateButtonStyles();
}
function toggleHeadphones() {
isHeadphonesMuted = !isHeadphonesMuted;
const audio = document.querySelector('audio#audioStream');
if (audio) {
audio.muted = isHeadphonesMuted;
}
if (isHeadphonesMuted && !isMicMuted) {
toggleMic(); // Выключение наушников мутит микрофон
}
updateButtonStyles();
}
function updateButtonStyles() {
const micButton = document.querySelector('#mic-toggle');
const headphoneButton = document.querySelector('#headphone-toggle');
if (micButton) {
const micState = globalStream && globalStream.getAudioTracks().length > 0 ? !globalStream.getAudioTracks()[0].enabled : isMicMuted;
isMicMuted = micState;
micButton.style.background = isMicMuted ? '#ff4d4d' : '#00ff9d';
micButton.style.textDecoration = isMicMuted ? 'line-through' : 'none';
micButton.style.boxShadow = `0 0 10px ${isMicMuted ? '#ff4d4d' : '#00ff9d'}`;
}
if (headphoneButton) {
headphoneButton.style.background = isHeadphonesMuted ? '#ff4d4d' : '#00ff9d';
headphoneButton.style.textDecoration = isHeadphonesMuted ? 'line-through' : 'none';
headphoneButton.style.boxShadow = `0 0 10px ${isHeadphonesMuted ? '#ff4d4d' : '#00ff9d'}`;
}
}
// ### UI элементы
function createVoiceHints() {
const wrapper = document.createElement('div');
wrapper.className = 'voice-hint-wrapper';
wrapper.style.cssText = `margin-left: 5px; display: ${isVoiceControlEnabled ? 'inline-block' : 'none'};`;
const trigger = document.createElement('span');
trigger.textContent = 'Подсказка';
trigger.style.cssText = `font-size: 12px; color: #bbb; cursor: help; padding: 2px 5px; background: #444; border-radius: 5px; transition: color 0.2s ease;`;
const content = document.createElement('div');
content.style.cssText = `position: absolute; background: rgba(43, 43, 43, 0.95); color: #fff; padding: 8px; border-radius: 6px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); font-size: 11px; display: none; transform: translateY(5px); opacity: 0; transition: all 0.3s ease; z-index: 1000;`;
content.innerHTML = `<b>Голосовые команды:</b><br><b>пропустить: </b>${VOICE_COMMANDS.skip.join('/')}<br><b>начать: </b>${VOICE_COMMANDS.start.join('/')}<br><b>остановить: </b>${VOICE_COMMANDS.stop.join('/')}`;
wrapper.appendChild(trigger);
wrapper.appendChild(content);
trigger.addEventListener('mouseenter', () => {
trigger.style.color = '#fff';
content.style.display = 'block';
setTimeout(() => { content.style.opacity = '1'; content.style.transform = 'translateY(0)'; }, 10);
});
trigger.addEventListener('mouseleave', () => {
trigger.style.color = '#bbb';
content.style.opacity = '0';
content.style.transform = 'translateY(5px)';
setTimeout(() => content.style.display = 'none', 300);
});
voiceHintElement = wrapper;
return wrapper;
}
function createConversationCounter() {
const counterDiv = document.createElement('div');
counterDiv.id = 'conversation-counter';
counterDiv.style.cssText = `
margin-bottom: 15px;
text-align: center;
position: relative;
`;
const counterSpan = document.createElement('span');
counterSpan.textContent = `Разговоров: ${settings.conversationCount}`;
counterSpan.style.cssText = `
color: #00ff9d;
font-size: 16px;
font-weight: bold;
text-shadow: 0 0 5px #00ff9d, 0 0 10px #00ff9d;
padding: 5px 10px;
background: rgba(0, 0, 0, 0.7);
border-radius: 8px;
cursor: default;
display: inline-block;
`;
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: absolute;
top: calc(100% + 5px);
left: 50%;
transform: translateX(-50%);
background: rgba(43, 43, 43, 0.95);
color: #fff;
padding: 10px;
border-radius: 6px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
font-size: 12px;
display: none;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1000;
white-space: nowrap;
`;
tooltip.innerHTML = `
Из них дольше:<br>
5 минут: ${settings.conversationStats.over5min}<br>
15 минут: ${settings.conversationStats.over15min}<br>
30 минут: ${settings.conversationStats.over30min}<br>
1 часа: ${settings.conversationStats.over1hour}<br>
2 часов: ${settings.conversationStats.over2hours}<br>
3 часов: ${settings.conversationStats.over3hours}<br>
5 часов: ${settings.conversationStats.over5hours}
`;
counterDiv.appendChild(counterSpan);
counterDiv.appendChild(tooltip);
counterSpan.addEventListener('mouseenter', () => {
tooltip.style.display = 'block';
setTimeout(() => {
tooltip.style.opacity = '1';
}, 10);
});
counterSpan.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
setTimeout(() => {
tooltip.style.display = 'none';
}, 300);
});
return counterDiv;
}
function createSettingsUI() {
const container = document.createElement('div');
container.id = 'settings-container';
container.style.position = 'fixed';
container.style.top = '20px';
container.style.right = '20px';
container.style.zIndex = '9999';
container.style.background = 'linear-gradient(135deg, #2b2b2b, #1f1f1f)';
container.style.padding = '20px';
container.style.borderRadius = '15px';
container.style.boxShadow = '0 5px 20px rgba(0, 0, 0, 0.7)';
container.style.border = '2px solid #ff007a';
container.style.width = '250px';
container.style.color = '#fff';
container.style.fontFamily = "'Segoe UI', Arial, sans-serif";
container.style.transition = 'transform 0.3s ease';
const header = document.createElement('h3');
header.textContent = 'Настройки';
header.style.margin = '0 0 15px';
header.style.fontSize = '20px';
header.style.color = '#ff007a';
header.style.textAlign = 'center';
header.style.textTransform = 'uppercase';
header.style.letterSpacing = '2px';
container.appendChild(header);
container.appendChild(createConversationCounter());
const audioControls = document.createElement('div');
audioControls.style.display = 'flex';
audioControls.style.gap = '10px';
audioControls.style.marginBottom = '20px';
audioControls.style.justifyContent = 'center';
const micButton = document.createElement('button');
micButton.id = 'mic-toggle';
micButton.innerHTML = '🎤';
micButton.style.width = '40px';
micButton.style.height = '40px';
micButton.style.borderRadius = '50%';
micButton.style.background = '#00ff9d';
micButton.style.border = 'none';
micButton.style.cursor = 'pointer';
micButton.style.fontSize = '20px';
micButton.style.display = 'flex';
micButton.style.alignItems = 'center';
micButton.style.justifyContent = 'center';
micButton.style.boxShadow = '0 0 10px #00ff9d';
micButton.style.transition = 'all 0.3s ease';
micButton.addEventListener('click', toggleMic);
const headphoneButton = document.createElement('button');
headphoneButton.id = 'headphone-toggle';
headphoneButton.innerHTML = '🎧';
headphoneButton.style.width = '40px';
headphoneButton.style.height = '40px';
headphoneButton.style.borderRadius = '50%';
headphoneButton.style.background = '#00ff9d';
headphoneButton.style.border = 'none';
headphoneButton.style.cursor = 'pointer';
headphoneButton.style.fontSize = '20px';
headphoneButton.style.display = 'flex';
headphoneButton.style.alignItems = 'center';
headphoneButton.style.justifyContent = 'center';
headphoneButton.style.boxShadow = '0 0 10px #00ff9d';
headphoneButton.style.transition = 'all 0.3s ease';
headphoneButton.addEventListener('click', toggleHeadphones);
audioControls.appendChild(micButton);
audioControls.appendChild(headphoneButton);
container.appendChild(audioControls);
const toggleWrapper = document.createElement('div');
toggleWrapper.style.display = 'flex';
toggleWrapper.style.alignItems = 'center';
toggleWrapper.style.gap = '15px';
toggleWrapper.style.marginBottom = '20px';
const label = document.createElement('label');
label.style.position = 'relative';
label.style.display = 'inline-block';
label.style.width = '70px';
label.style.height = '34px';
const toggleInput = document.createElement('input');
toggleInput.type = 'checkbox';
toggleInput.style.display = 'none';
toggleInput.checked = isAutoModeEnabled;
const slider = document.createElement('span');
slider.className = 'slider';
slider.style.position = 'absolute';
slider.style.cursor = 'pointer';
slider.style.top = '0';
slider.style.left = '0';
slider.style.right = '0';
slider.style.bottom = '0';
slider.style.background = isAutoModeEnabled ? '#00ff9d' : '#555';
slider.style.transition = 'background 0.4s';
slider.style.borderRadius = '34px';
slider.style.boxShadow = 'inset 0 2px 5px rgba(0,0,0,0.5)';
const sliderCircle = document.createElement('span');
sliderCircle.className = 'slider-circle';
sliderCircle.style.position = 'absolute';
sliderCircle.style.height = '26px';
sliderCircle.style.width = '26px';
sliderCircle.style.left = isAutoModeEnabled ? '40px' : '4px';
sliderCircle.style.bottom = '4px';
sliderCircle.style.background = '#fff';
sliderCircle.style.transition = 'left 0.4s';
sliderCircle.style.borderRadius = '50%';
sliderCircle.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
slider.appendChild(sliderCircle);
label.appendChild(toggleInput);
label.appendChild(slider);
const toggleLabel = document.createElement('span');
toggleLabel.className = 'toggle-label';
toggleLabel.textContent = `Авторежим ${isAutoModeEnabled ? 'ВКЛ' : 'ВЫКЛ'}`;
toggleLabel.style.color = isAutoModeEnabled ? '#00ff9d' : '#ff4d4d';
toggleLabel.style.fontSize = '16px';
toggleLabel.style.fontWeight = 'bold';
toggleLabel.style.textShadow = `0 0 5px ${isAutoModeEnabled ? '#00ff9d' : '#ff4d4d'}`;
toggleWrapper.appendChild(label);
toggleWrapper.appendChild(toggleLabel);
container.appendChild(toggleWrapper);
const audioSettings = document.createElement('div');
audioSettings.style.display = 'flex';
audioSettings.style.flexDirection = 'column';
audioSettings.style.gap = '15px';
function createToggle(labelText, key) {
const div = document.createElement('div');
const toggleDiv = document.createElement('div');
toggleDiv.style.display = 'flex';
toggleDiv.style.alignItems = 'center';
toggleDiv.style.gap = '10px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.appearance = 'none';
checkbox.style.width = '20px';
checkbox.style.height = '20px';
checkbox.style.background = settings[key] ? '#00ff9d' : '#555';
checkbox.style.borderRadius = '5px';
checkbox.style.cursor = 'pointer';
checkbox.style.transition = 'background 0.3s';
checkbox.style.boxShadow = 'inset 0 2px 5px rgba(0,0,0,0.5)';
checkbox.checked = settings[key];
const labelSpan = document.createElement('span');
labelSpan.textContent = labelText;
labelSpan.style.fontSize = '14px';
labelSpan.style.color = '#fff';
labelSpan.style.fontWeight = 'bold';
labelSpan.style.textShadow = '0 0 3px rgba(255,255,255,0.5)';
toggleDiv.appendChild(checkbox);
toggleDiv.appendChild(labelSpan);
div.appendChild(toggleDiv);
let volumeContainer = null;
let pitchContainer = null;
if (key === 'enableLoopback') {
volumeContainer = document.createElement('div');
volumeContainer.style.display = settings.enableLoopback ? 'block' : 'none';
volumeContainer.style.marginTop = '10px';
const volumeLabel = document.createElement('span');
volumeLabel.textContent = `Громкость самопрослушивания: ${settings.gainValue.toFixed(1)}`;
volumeLabel.style.fontSize = '14px';
volumeLabel.style.color = '#fff';
volumeLabel.style.fontWeight = 'bold';
volumeLabel.style.textShadow = '0 0 3px rgba(255,255,255,0.5)';
const volumeSlider = document.createElement('input');
volumeSlider.type = 'range';
volumeSlider.min = '0.1';
volumeSlider.max = '3.0';
volumeSlider.step = '0.1';
volumeSlider.value = settings.gainValue;
volumeSlider.style.width = '100%';
volumeSlider.style.height = '8px';
volumeSlider.style.background = `linear-gradient(to right, #ff007a ${((settings.gainValue - 0.1) / 2.9) * 100}%, #555 0%)`;
volumeSlider.style.borderRadius = '5px';
volumeSlider.style.outline = 'none';
volumeSlider.style.cursor = 'pointer';
volumeSlider.style.appearance = 'none';
volumeContainer.appendChild(volumeLabel);
volumeContainer.appendChild(volumeSlider);
volumeSlider.addEventListener('input', () => {
settings.gainValue = parseFloat(volumeSlider.value);
volumeLabel.textContent = `Громкость самопрослушивания: ${settings.gainValue.toFixed(1)}`;
saveSetting('gainValue', settings.gainValue);
if (gainNode) gainNode.gain.value = settings.gainValue;
volumeSlider.style.background = `linear-gradient(to right, #ff007a ${((settings.gainValue - 0.1) / 2.9) * 100}%, #555 0%)`;
});
}
if (key === 'voicePitch') {
pitchContainer = document.createElement('div');
pitchContainer.style.display = settings.voicePitch ? 'block' : 'none';
pitchContainer.style.marginTop = '10px';
const pitchLabel = document.createElement('span');
pitchLabel.textContent = `0 - обычный голос, 0.40 - очень низкий: ${settings.pitchLevel.toFixed(2)}`;
pitchLabel.style.fontSize = '14px';
pitchLabel.style.color = '#fff';
pitchLabel.style.fontWeight = 'bold';
pitchLabel.style.textShadow = '0 0 3px rgba(255,255,255,0.5)';
const pitchSlider = document.createElement('input');
pitchSlider.type = 'range';
pitchSlider.min = '0';
pitchSlider.max = '0.4';
pitchSlider.step = '0.01';
pitchSlider.value = settings.pitchLevel;
pitchSlider.style.width = '100%';
pitchSlider.style.height = '8px';
pitchSlider.style.background = `linear-gradient(to right, #ff007a ${(settings.pitchLevel / 0.4) * 100}%, #555 0%)`;
pitchSlider.style.borderRadius = '5px';
pitchSlider.style.outline = 'none';
pitchSlider.style.cursor = 'pointer';
pitchSlider.style.appearance = 'none';
pitchContainer.appendChild(pitchLabel);
pitchContainer.appendChild(pitchSlider);
pitchSlider.addEventListener('input', () => {
settings.pitchLevel = parseFloat(pitchSlider.value);
pitchLabel.textContent = `0 - обычный голос, 0.40 - очень низкий: ${settings.pitchLevel.toFixed(2)}`;
saveSetting('pitchLevel', settings.pitchLevel);
updatePitchLevel(settings.pitchLevel);
pitchSlider.style.background = `linear-gradient(to right, #ff007a ${(settings.pitchLevel / 0.4) * 100}%, #555 0%)`;
});
}
checkbox.addEventListener('change', () => {
settings[key] = checkbox.checked;
saveSetting(key, checkbox.checked);
checkbox.style.background = checkbox.checked ? '#00ff9d' : '#555';
if (key === 'enableLoopback') {
if (checkbox.checked && globalStream) enableSelfListening(globalStream);
else if (audioContext) audioContext.close();
if (volumeContainer) volumeContainer.style.display = checkbox.checked ? 'block' : 'none';
} else if (key === 'voiceControl') {
isVoiceControlEnabled = checkbox.checked;
if (voiceHintElement) voiceHintElement.style.display = checkbox.checked ? 'inline-block' : 'none';
if (checkbox.checked) {
if (!recognition) initSpeechRecognition();
recognition.start();
} else if (recognition) recognition.stop();
} else if (key === 'autoVolume') {
if (checkbox.checked) {
const audio = document.querySelector('audio#audioStream');
if (audio && audio.srcObject) setupAutoVolume(audio.srcObject);
} else {
if (remoteAudioContext) remoteAudioContext.close();
if (volumeCheckIntervalId) clearInterval(volumeCheckIntervalId);
}
} else if (key === 'voicePitch') {
updatePitchEffect(checkbox.checked);
if (pitchContainer) pitchContainer.style.display = checkbox.checked ? 'block' : 'none';
}
});
if (key === 'voiceControl') div.appendChild(createVoiceHints());
if (key === 'enableLoopback' && volumeContainer) div.appendChild(volumeContainer);
if (key === 'voicePitch' && pitchContainer) div.appendChild(pitchContainer);
return div;
}
audioSettings.appendChild(createToggle('Самопрослушивание', 'enableLoopback'));
audioSettings.appendChild(createToggle('Автогромкость микрофона', 'autoGainControl'));
audioSettings.appendChild(createToggle('Автогромкость собеседника', 'autoVolume'));
audioSettings.appendChild(createToggle('Шумоподавление', 'noiseSuppression'));
audioSettings.appendChild(createToggle('Эхоподавление', 'echoCancellation'));
audioSettings.appendChild(createToggle('Низкий голос', 'voicePitch'));
audioSettings.appendChild(createToggle('Голосовое управление', 'voiceControl'));
container.appendChild(audioSettings);
container.appendChild(createThemeSelector());
document.body.appendChild(container);
const styleSheet = document.createElement('style');
styleSheet.textContent = `
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
background: #ff007a;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 5px #ff007a;
}
select:hover {
background: #333;
}
select:focus {
border-color: #00ff9d;
}
`;
document.head.appendChild(styleSheet);
container.addEventListener('mouseover', () => container.style.transform = 'scale(1.02)');
container.addEventListener('mouseout', () => container.style.transform = 'scale(1)');
toggleInput.addEventListener('change', (e) => toggleAutoMode(e.target.checked));
}
// ### Инициализация
function initObserver() {
observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
checkAndClickButton();
const audio = document.querySelector('audio#audioStream');
if (audio && audio.srcObject && settings.autoVolume) setupAutoVolume(audio.srcObject);
const timerElement = document.querySelector('.timer-label');
if (timerElement && timerElement.textContent === '00:00' && !conversationTimer) {
startConversationTimer();
}
if (!timerElement && conversationTimer) {
stopConversationTimer();
}
const stopButton = document.querySelector('button.btn.btn-lg.stop-talk-button');
if (stopButton && !stopButton.dataset.listenerAdded) {
stopButton.addEventListener('click', () => {
setTimeout(() => {
const confirmButton = document.querySelector('button.swal2-confirm.swal2-styled');
if (confirmButton && !confirmButton.dataset.listenerAdded) {
confirmButton.addEventListener('click', playNotificationOnEnd);
confirmButton.dataset.listenerAdded = 'true';
}
}, 500);
});
stopButton.dataset.listenerAdded = 'true';
}
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
navigator.mediaDevices.getUserMedia = ((original) => {
return async (constraints) => {
if (constraints?.audio) {
constraints.audio = {
...constraints.audio,
autoGainControl: settings.autoGainControl,
noiseSuppression: settings.noiseSuppression,
echoCancellation: settings.echoCancellation
};
}
const stream = await original.call(navigator.mediaDevices, constraints);
micStream = stream;
const processedStream = await createPitchShiftedStream(stream);
globalStream = processedStream;
if (globalStream && isMicMuted) {
globalStream.getAudioTracks().forEach(track => {
track.enabled = false;
});
}
if (settings.enableLoopback) enableSelfListening(processedStream);
return processedStream;
};
})(navigator.mediaDevices.getUserMedia);
const originalSet = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'srcObject').set;
Object.defineProperty(HTMLMediaElement.prototype, 'srcObject', {
set: function(stream) {
originalSet.call(this, stream);
if (this.id === 'audioStream' && stream && settings.autoVolume) setupAutoVolume(stream);
}
});
async function init() {
console.log('Инициализация скрипта...');
createSettingsUI();
applyTheme(settings.selectedTheme);
checkAndClickButton();
initObserver();
await initSpeechRecognition();
console.log('Инициализация завершена');
}
window.addEventListener('load', () => {
console.log('Страница загружена, запускаем init');
init();
});
})();