На youtube я наткнулся на решение программное, которое переводит речь в текст. По прошлым постам понятно почему мне это интересно. Я пользовался триалом месяц потом пришла пора покупать. Видимо я угроза свободного мира. Мне это купить нельзя. Я привык к этой софтине за месяц поэтому решил написать свою. Писал на шарпе думал так проще. Вообще-то я делфист, но какая разница.

Мне так и не удалось добиться того, чтобы вставляла прямо в активное окно. Может, кто-нибудь в комментарии подскажет, как это сделать? Сейчас я действую через буфер обмена. Я не умею прятать подкат, поэтому выкладываю все, как всегда на Пастебин.

using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using NAudio.Wave;
using Newtonsoft.Json.Linq;
namespace VkVoiceTyper
{
class Program
{
// === СЕРВИСНЫЙ ТОКЕН VK CLOUD ===
// Вставьте токен из настроек проекта VK Cloud
private const string ServiceToken = "ВАШ_СЕРВИСНЫЙ_ТОКЕН_VC_...";
private const string OutputFileName = "voice_temp.wav";
// === ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ===
private static WaveInEvent? waveIn;
private static WaveFileWriter? writer;
private static bool isRecording = false;
private static bool isProcessing = false;
// === HOOK API ===
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private const int VK_RCONTROL = 0xA3;
private const int VK_ESCAPE = 0x1B;
private static LowLevelKeyboardProc proc = HookCallback;
private static IntPtr hookID = IntPtr.Zero;
// HttpClient с таймаутом
private static readonly HttpClient httpClient = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(15)
};
static void Main(string[] args)
{
Console.WriteLine("🎙️ VK VoiceTyper (Service Token)");
Console.WriteLine("👉 Удерживайте ПРАВЫЙ CTRL для записи");
Console.WriteLine("🔚 ESC — выход");
Console.WriteLine("================================\n");
// Проверка токена при старте
if (string.IsNullOrEmpty(ServiceToken) || ServiceToken == "ВАШ_СЕРВИСНЫЙ_ТОКЕН_VC_...")
{
Console.WriteLine("❌ Ошибка: Вставьте сервисный токен в код!");
Console.WriteLine("Нажмите любую клавишу для выхода...");
Console.ReadKey();
return;
}
Console.WriteLine("✅ Токен загружен. Сервис готов.\n");
hookID = SetHook(proc);
}
// === HOOK CALLBACK ===
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
int vkCode = Marshal.ReadInt32(lParam);
if (wParam == (IntPtr)WM_KEYDOWN && vkCode == VK_RCONTROL)
{
if (!isRecording && !isProcessing)
Task.Run(() => StartRecordingAsync());
}
else if (wParam == (IntPtr)WM_KEYUP && vkCode == VK_RCONTROL)
{
if (isRecording)
Task.Run(() => StopRecordingAndProcessAsync());
}
else if (wParam == (IntPtr)WM_KEYDOWN && vkCode == VK_ESCAPE)
{
UnhookWindowsHookEx(hookID);
Application.Exit();
return (IntPtr)1;
}
}
return CallNextHookEx(hookID, nCode, wParam, lParam);
}
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using Process curProcess = Process.GetCurrentProcess();
using ProcessModule? curModule = curProcess.MainModule;
return SetWindowsHookEx(WH_KEYBOARD_LL, proc,
GetModuleHandle(curModule?.ModuleName), 0);
}
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string? lpModuleName);
// === ЗАПИСЬ ===
private static void StartRecordingAsync()
{
if (isRecording) return;
isRecording = true;
Console.WriteLine("🔴 Запись... (отпустите RCtrl)");
var waveFormat = new WaveFormat(16000, 16, 1);
waveIn = new WaveInEvent { WaveFormat = waveFormat };
writer = new WaveFileWriter(OutputFileName, waveIn.WaveFormat);
waveIn.DataAvailable += (s, e) => writer?.Write(e.Buffer, 0, e.BytesRecorded);
waveIn.StartRecording();
}
private static async Task StopRecordingAndProcessAsync()
{
if (!isRecording) return;
isRecording = false;
Console.WriteLine("⏹️ Остановка...");
waveIn?.StopRecording();
waveIn?.Dispose();
writer?.Dispose();
waveIn = null;
writer = null;
await Task.Delay(200);
if (!File.Exists(OutputFileName))
{
Console.WriteLine("⚠️ Файл не создан");
return;
}
long fileSize = new FileInfo(OutputFileName).Length;
if (fileSize < 1000)
{
Console.WriteLine($"⚠️ Слишком короткая запись ({fileSize} байт)");
return;
}
isProcessing = true;
Console.WriteLine("🔄 Распознавание...");
try
{
string? recognizedText = await RecognizeSpeechAsync(ServiceToken, OutputFileName);
if (!string.IsNullOrWhiteSpace(recognizedText))
{
Console.WriteLine($"✅ Распознано: \"{recognizedText}\"");
InsertTextInStaThread(recognizedText);
}
else
{
Console.WriteLine("⚠️ Текст не распознан");
}
}
catch (TaskCanceledException)
{
Console.WriteLine("❌ Таймаут! Сервер не ответил за 15 секунд.");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Ошибка: {ex.Message}");
if (ex.InnerException != null)
Console.WriteLine($" Детали: {ex.InnerException.Message}");
}
finally
{
isProcessing = false;
if (File.Exists(OutputFileName))
File.Delete(OutputFileName);
}
}
// === ВСТАВКА ТЕКСТА ===
private static void InsertTextInStaThread(string text)
{
Thread staThread = new Thread(() =>
{
try
{
Clipboard.SetText(text, TextDataFormat.Text);
Thread.Sleep(150);
Console.WriteLine("📋 Текст скопирован в буфер обмена (Ctrl+V для вставки)");
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ Ошибка буфера: {ex.Message}");
}
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join();
}
// === РАСПОЗНАВАНИЕ (с сервисным токеном) ===
private static async Task<string?> RecognizeSpeechAsync(string token, string filePath)
{
try
{
byte[] fileBytes = File.ReadAllBytes(filePath);
var content = new ByteArrayContent(fileBytes);
content.Headers.ContentType = new MediaTypeHeaderValue("audio/wave");
var request = new HttpRequestMessage(HttpMethod.Post, "https://voice.mcs.mail.ru/asr");
// ✅ Сервисный токен используется напрямую
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = content;
Console.WriteLine("📤 Отправка в VK Cloud Voice...");
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var jobject = JObject.Parse(json);
if (jobject["result"]?["texts"] is JArray texts && texts.Count > 0)
{
return texts[0]["punctuated_text"]?.ToString();
}
else
{
Console.WriteLine($"⚠️ Ответ сервера: {json}");
return null;
}
}
else
{
var error = await response.Content.ReadAsStringAsync();
Console.WriteLine($"🔌 API ошибка: {response.StatusCode}");
Console.WriteLine($" Детали: {error}");
// Проверка на истёкший токен
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Console.WriteLine("⚠️ Токен недействителен. Получите новый в VK Cloud.");
}
return null;
}
}
catch (Exception ex)
{
Console.WriteLine($"🔌 Ошибка распознавания: {ex.Message}");
throw;
}
}
}
}
Обратите внимание на эти строки. Токен получается в личном кабинете.



Для уточнения можно спросить у Алисы. Изначально для публикации статьи у меня была такая цель. Иронично шантажировать разработчиков. Мол, продайте мне ключ. Тогда я удалю исходный код из открытого доступа. Но я написал эту статью, используя свой инструмент. Выходит, что он гораздо удобнее, по крайней мере для меня. Поэтому исходный код я не удалю.

Еще один момент. Распознавание речи вроде как бесплатный продукт. Но к концу написания статьи у меня ушло уже три рубля. На файлообменник. Я положу собранный проект. Благо, что не все мы программисты. Если вдруг деньги будут сильно уходить, я сменю ключ, имейте в виду. Все скомпилировано с моим токеном. Запускать надо под администратором. Соответственно, используется мой личный счет. Может, кто-то в комментариях подскажет, где я был не прав? Всем приятного использования.
