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

главное окно
главное окно

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

  1. using System;

  2. using System.Diagnostics;

  3. using System.IO;

  4. using System.Net.Http;

  5. using System.Net.Http.Headers;

  6. using System.Runtime.InteropServices;

  7. using System.Threading;

  8. using System.Threading.Tasks;

  9. using System.Windows.Forms;

  10. using NAudio.Wave;

  11. using Newtonsoft.Json.Linq;

  12.  

  13. namespace VkVoiceTyper

  14. {

  15. class Program

  16. {

  17. // === СЕРВИСНЫЙ ТОКЕН VK CLOUD ===

  18. // Вставьте токен из настроек проекта VK Cloud

  19. private const string ServiceToken = "ВАШ_СЕРВИСНЫЙ_ТОКЕН_VC_...";

  20.  

  21. private const string OutputFileName = "voice_temp.wav";

  22.  

  23. // === ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ===

  24. private static WaveInEvent? waveIn;

  25. private static WaveFileWriter? writer;

  26. private static bool isRecording = false;

  27. private static bool isProcessing = false;

  28.  

  29. // === HOOK API ===

  30. private const int WH_KEYBOARD_LL = 13;

  31. private const int WM_KEYDOWN = 0x0100;

  32. private const int WM_KEYUP = 0x0101;

  33. private const int VK_RCONTROL = 0xA3;

  34. private const int VK_ESCAPE = 0x1B;

  35.  

  36. private static LowLevelKeyboardProc proc = HookCallback;

  37. private static IntPtr hookID = IntPtr.Zero;

  38.  

  39. // HttpClient с таймаутом

  40. private static readonly HttpClient httpClient = new HttpClient()

  41. {

  42. Timeout = TimeSpan.FromSeconds(15)

  43. };

  44.  

  45. static void Main(string[] args)

  46. {

  47. Console.WriteLine("🎙️ VK VoiceTyper (Service Token)");

  48. Console.WriteLine("👉 Удерживайте ПРАВЫЙ CTRL для записи");

  49. Console.WriteLine("🔚 ESC — выход");

  50. Console.WriteLine("================================\n");

  51.  

  52. // Проверка токена при старте

  53. if (string.IsNullOrEmpty(ServiceToken) || ServiceToken == "ВАШ_СЕРВИСНЫЙ_ТОКЕН_VC_...")

  54. {

  55. Console.WriteLine("❌ Ошибка: Вставьте сервисный токен в код!");

  56. Console.WriteLine("Нажмите любую клавишу для выхода...");

  57. Console.ReadKey();

  58. return;

  59. }

  60.  

  61. Console.WriteLine("✅ Токен загружен. Сервис готов.\n");

  62.  

  63. hookID = SetHook(proc);

  64. Application.Run();

  65. }

  66.  

  67. // === HOOK CALLBACK ===

  68. private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)

  69. {

  70. if (nCode >= 0)

  71. {

  72. int vkCode = Marshal.ReadInt32(lParam);

  73.  

  74. if (wParam == (IntPtr)WM_KEYDOWN && vkCode == VK_RCONTROL)

  75. {

  76. if (!isRecording && !isProcessing)

  77. Task.Run(() => StartRecordingAsync());

  78. }

  79. else if (wParam == (IntPtr)WM_KEYUP && vkCode == VK_RCONTROL)

  80. {

  81. if (isRecording)

  82. Task.Run(() => StopRecordingAndProcessAsync());

  83. }

  84. else if (wParam == (IntPtr)WM_KEYDOWN && vkCode == VK_ESCAPE)

  85. {

  86. UnhookWindowsHookEx(hookID);

  87. Application.Exit();

  88. return (IntPtr)1;

  89. }

  90. }

  91. return CallNextHookEx(hookID, nCode, wParam, lParam);

  92. }

  93.  

  94. private static IntPtr SetHook(LowLevelKeyboardProc proc)

  95. {

  96. using Process curProcess = Process.GetCurrentProcess();

  97. using ProcessModule? curModule = curProcess.MainModule;

  98. return SetWindowsHookEx(WH_KEYBOARD_LL, proc,

  99. GetModuleHandle(curModule?.ModuleName), 0);

  100. }

  101.  

  102. private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

  103.  

  104. [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]

  105. private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

  106.  

  107. [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]

  108. [return: MarshalAs(UnmanagedType.Bool)]

  109. private static extern bool UnhookWindowsHookEx(IntPtr hhk);

  110.  

  111. [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]

  112. private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

  113.  

  114. [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]

  115. private static extern IntPtr GetModuleHandle(string? lpModuleName);

  116.  

  117. // === ЗАПИСЬ ===

  118. private static void StartRecordingAsync()

  119. {

  120. if (isRecording) return;

  121. isRecording = true;

  122. Console.WriteLine("🔴 Запись... (отпустите RCtrl)");

  123.  

  124. var waveFormat = new WaveFormat(16000, 16, 1);

  125. waveIn = new WaveInEvent { WaveFormat = waveFormat };

  126. writer = new WaveFileWriter(OutputFileName, waveIn.WaveFormat);

  127.  

  128. waveIn.DataAvailable += (s, e) => writer?.Write(e.Buffer, 0, e.BytesRecorded);

  129. waveIn.StartRecording();

  130. }

  131.  

  132. private static async Task StopRecordingAndProcessAsync()

  133. {

  134. if (!isRecording) return;

  135. isRecording = false;

  136. Console.WriteLine("⏹️ Остановка...");

  137.  

  138. waveIn?.StopRecording();

  139. waveIn?.Dispose();

  140. writer?.Dispose();

  141. waveIn = null;

  142. writer = null;

  143.  

  144. await Task.Delay(200);

  145.  

  146. if (!File.Exists(OutputFileName))

  147. {

  148. Console.WriteLine("⚠️ Файл не создан");

  149. return;

  150. }

  151.  

  152. long fileSize = new FileInfo(OutputFileName).Length;

  153. if (fileSize < 1000)

  154. {

  155. Console.WriteLine($"⚠️ Слишком короткая запись ({fileSize} байт)");

  156. return;

  157. }

  158.  

  159. isProcessing = true;

  160. Console.WriteLine("🔄 Распознавание...");

  161.  

  162. try

  163. {

  164. string? recognizedText = await RecognizeSpeechAsync(ServiceToken, OutputFileName);

  165.  

  166. if (!string.IsNullOrWhiteSpace(recognizedText))

  167. {

  168. Console.WriteLine($"✅ Распознано: \"{recognizedText}\"");

  169. InsertTextInStaThread(recognizedText);

  170. }

  171. else

  172. {

  173. Console.WriteLine("⚠️ Текст не распознан");

  174. System.Media.SystemSounds.Beep.Play();

  175. }

  176. }

  177. catch (TaskCanceledException)

  178. {

  179. Console.WriteLine("❌ Таймаут! Сервер не ответил за 15 секунд.");

  180. System.Media.SystemSounds.Beep.Play();

  181. }

  182. catch (Exception ex)

  183. {

  184. Console.WriteLine($"❌ Ошибка: {ex.Message}");

  185. if (ex.InnerException != null)

  186. Console.WriteLine($" Детали: {ex.InnerException.Message}");

  187. System.Media.SystemSounds.Beep.Play();

  188. }

  189. finally

  190. {

  191. isProcessing = false;

  192. if (File.Exists(OutputFileName))

  193. File.Delete(OutputFileName);

  194. }

  195. }

  196.  

  197. // === ВСТАВКА ТЕКСТА ===

  198. private static void InsertTextInStaThread(string text)

  199. {

  200. Thread staThread = new Thread(() =>

  201. {

  202. try

  203. {

  204. Clipboard.SetText(text, TextDataFormat.Text);

  205. Thread.Sleep(150);

  206. Console.WriteLine("📋 Текст скопирован в буфер обмена (Ctrl+V для вставки)");

  207. }

  208. catch (Exception ex)

  209. {

  210. Console.WriteLine($"⚠️ Ошибка буфера: {ex.Message}");

  211. }

  212. });

  213.  

  214. staThread.SetApartmentState(ApartmentState.STA);

  215. staThread.Start();

  216. staThread.Join();

  217. }

  218.  

  219. // === РАСПОЗНАВАНИЕ (с сервисным токеном) ===

  220. private static async Task<string?> RecognizeSpeechAsync(string token, string filePath)

  221. {

  222. try

  223. {

  224. byte[] fileBytes = File.ReadAllBytes(filePath);

  225. var content = new ByteArrayContent(fileBytes);

  226. content.Headers.ContentType = new MediaTypeHeaderValue("audio/wave");

  227.  

  228. var request = new HttpRequestMessage(HttpMethod.Post, "https://voice.mcs.mail.ru/asr");

  229.  

  230. // ✅ Сервисный токен используется напрямую

  231. request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

  232. request.Content = content;

  233.  

  234. Console.WriteLine("📤 Отправка в VK Cloud Voice...");

  235. var response = await httpClient.SendAsync(request);

  236.  

  237. if (response.IsSuccessStatusCode)

  238. {

  239. var json = await response.Content.ReadAsStringAsync();

  240. var jobject = JObject.Parse(json);

  241.  

  242. if (jobject["result"]?["texts"] is JArray texts && texts.Count > 0)

  243. {

  244. return texts[0]["punctuated_text"]?.ToString();

  245. }

  246. else

  247. {

  248. Console.WriteLine($"⚠️ Ответ сервера: {json}");

  249. return null;

  250. }

  251. }

  252. else

  253. {

  254. var error = await response.Content.ReadAsStringAsync();

  255. Console.WriteLine($"🔌 API ошибка: {response.StatusCode}");

  256. Console.WriteLine($" Детали: {error}");

  257.  

  258. // Проверка на истёкший токен

  259. if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)

  260. {

  261. Console.WriteLine("⚠️ Токен недействителен. Получите новый в VK Cloud.");

  262. }

  263.  

  264. return null;

  265. }

  266. }

  267. catch (Exception ex)

  268. {

  269. Console.WriteLine($"🔌 Ошибка распознавания: {ex.Message}");

  270. throw;

  271. }

  272. }

  273. }

  274. }

Обратите внимание на эти строки. Токен получается в личном кабинете.

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

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