.NET’te AI Desteğiyle Text-to-Speech (TTS) ve Speech-to-Text (STT) Operasyonları
Merhaba,
Bu içeriğimizde ses ve metin arasında dönüşüm işlemlerine karşılık gelen Text-to-Speech (TTS) ve Speech-to-Text (STT) kavramlarını ele alacak ve .NET ekosisteminde bu kavramlar doğrultusunda pratiksel operasyonları nasıl gerçekleştirebileceğimizi deneyimleyeceğiz.
Text-to-Speech (TTS) ve Speech-to-Text (STT) Nedir?
İlk olarak bu kavramları tam olarak izah ederek başlayalım istiyorum. Bunun için aşağıdaki tablo üzerinden mukayeseli bir incelemede bulunmamız yeterli olacaktır kanaatindeyim;
| Kriter | Text-to-Speech (TTS) | Speech-to-Text (STT) |
|---|---|---|
| Anlamı | Yazılı metni sesli konuşmaya dönüştürür. | Konuşulan sesi yazılı metne dönüştürür. |
| Girdi Türü | Metin | Ses (konuşma, video, mikrofon) |
| Çıktı Türü | Ses | Metin |
| Kullanım Alanları |
|
|
| Kullanıcı Yararları |
|
|
| Teknik Zorluklar | Doğal ve akıcı ses üretimi yapmak | Farklı aksanları, ses kalitesini ve arka plan seslerini ayırt etmek |
| Popüler Örnekler |
|
|
| Dil İşleme Yöntemi | Metni analiz edip telaffuz ve tonlama kurallarına göre seslendirir. | Ses dalgalarını analiz edip fonetik olarak metne dönüştürür. |
| Kimin İçin İdeal? |
|
|
Text-to-Speech (TTS) ve Speech-to-Text (STT), uygulamalarımızda yapay zeka destekli iletişimi kolaylaştırdığı için günlük hayatta ve iş süreçlerinde sıkça tercih edilmeye başlanmış teknolojilerdir.
Evet, bu yaklaşımların ne olduğunu tam teferruatlı masaya yatırdığımıza göre artık pratik olarak uygulamaya geçebiliriz. Her iki yaklaşım için gerekli olan modelleri Hugging Face üzerinden kullanacağız. Tabi burada bahsetmem gereken önemli bir husus vardır ki; o da, yapacağımız çalışmaların Semantic Kernel entegrasyonuyla değil de bizzat HuggingFace model API’leri üzerinden olacağıdır. Neden? diye sorarsanız eğer, Semantic Kernel’ın şu anda daha çok text generation, text classification vs. gibi metin tabanlı modeller için optimize edilmiş olması ve bundan kaynaklı Semantic Kernel üzerinden TTS ve STT modellerinin doğrudan çalışmasının şimdilik pek mümkün olmamasıdır.
Velhasıl…
Temel Yapılandırmalar
Şimdi bu çalışmaların yapılabilmesi için bir Asp.NET Core uygulaması oluşturalım ve üzerinde aşağıdaki gibi yapılandırmalarda bulunarak hali hazır bir zemin oluşturalım;
-
var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient("HuggingFaceAPI", httpClient => { httpClient.BaseAddress = new Uri("https://api-inference.huggingface.co"); httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, $"Bearer {builder.Configuration["HuggingFace:APIKey"]}"); });Modellerin API’lerine istekte bulunabilmek için header’ında ‘Authorization‘ key’ine karşılık bearer token’ı verilmiş ve base address’i de barındıran hali hazırda bir
HttpClientgetirecek olan yapılandırmada bulunalım. Burada içeriğin devamında, her iki modeli Hugging Face üzerinden kullanacağımızdan dolayı her ikisinde de base address’in aynı olduğunu, sadece endpoint’in path’inde modele göre değişikliklerin söz konusu olacağını göreceksiniz. O yüzden sürekli aynı işi yapacakHttpClientnesnesi oluşturmamak ve benzer request endpoint’ini yazmamak için bu şekilde yapılandırmada bulunmayı tercih ettim. -
builder.Services.AddCors(options => options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); })); . . . var app = builder.Build(); app.UseCors();Günün sonunda client’ı bir frontend uygulaması olarak tasarlayacağımızdan dolayı şimdiden CORS politikalarını da yapılandıralım.
-
app.UseDefaultFiles(); app.UseStaticFiles();
İçeriğimiz sürecinde client uygulamasını basitinden geliştirip vur geç yaparak hızlıca işimizi görebilemek için direkt
wwwrootdosyası içerisindedefault.html,default.html,index.htmlya daindex.htmlisimlerinden birinde static bir dosya olarak ayarlayacağız. Uygulamayı ayağa kaldırır kaldırmaz, tarayıcıda herhangi bir path’e ihtiyaç duyulmaksızın direkt host üzerinden client uygulamasının açılmasını istiyorsakUseDefaultFilesmiddleware’ini kullanmalıyız. Benzer şekilde wwwroot içerisindeki dosyalara erişimin olabilmesi için deUseStaticFilesmiddleware’ini. -
. . . builder.Services.AddDirectoryBrowser(); var app = builder.Build(); . . . string webRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "audio_outputs"); if (!Directory.Exists(webRootPath)) Directory.CreateDirectory(webRootPath); IFileProvider fileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.WebRootPath, "audio_outputs")); app.UseStaticFiles(new StaticFileOptions { FileProvider = fileProvider, RequestPath = "/audio_outputs" }); app.UseDirectoryBrowser(new DirectoryBrowserOptions { FileProvider = fileProvider, RequestPath = "/audio_outputs" });AI modelinin üreteceği çıktıları
wwwrootiçerisindeki bir klasörde tutmak isteyebiliriz. Misal olarak; Text-to-Speech neticesinde üretilen ses dosyasını audio_outputs isimli bir klasörde depolamak isteyeceğimizi varsayalım. Bunun için 10 ile 12. satır aralığında olduğu gibi bu klasörün var olup olmadığını kontrol edelim, yoksa oluşturalım. 16 ile 20. satır aralığında olduğu gibi de bu kontrol edilen klasörü static files olarak ayarlayalım ve 22 ile 26. satır aralığındaki gibi de yine bu klasör dizinini browser’dan girilebilecek şekilde yapılandıralım. Tabi tarayıcıdan static dosyaların erişilebilir kılınabilmesi için de 4. satırda olduğu gibiAddDirectoryBrowserservisini uygulamaya ekleyelim.
Evet, artık TTS ve STT çalışmaları için gerekli altyapıya sahip bir Asp.NET Core uygulamamız var diyebiliriz. Bu uygulamanın bütünsel olarak yapılandırma özetini aşağı bırakarak artık hususi olarak esas işlevlere odaklanabiliriz;
using AI_TextToSpeech_And_SpeechToText_Example;
using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("HuggingFaceAPI", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api-inference.huggingface.co");
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, $"Bearer {builder.Configuration["HuggingFace:APIKey"]}");
});
builder.Services.AddCors(options =>
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
}));
builder.Services.AddDirectoryBrowser();
var app = builder.Build();
app.UseCors();
#region index.html - style.css
app.UseDefaultFiles();
app.UseStaticFiles();
#endregion
#region wwwroot/audio_outputs
string webRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "audio_outputs");
if (!Directory.Exists(webRootPath))
Directory.CreateDirectory(webRootPath);
IFileProvider fileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.WebRootPath, "audio_outputs"));
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = "/audio_outputs"
});
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = fileProvider,
RequestPath = "/audio_outputs"
});
#endregion
//Endpoints...
app.Run();
.NET’te Text-to-Speech Uygulaması
Text-to-Speech için onlarca model arasından şimdilik ücretsiz kullanıma sahip olan facebook/mms-tts-tur modelini kullanıyor olacağız. Bu model, MMS (Massively Multilingual Speech) projesinin bir parçası olarak Meta (Facebook AI) tarafından geliştirilmiş ve Türkçe için eğitilmiş bir TTS modelidir. Yüzlerce dilde metinden doğal ses sentezi yapabilen bu model ile çok kaliteli ses dönüşümleri gerçekleştirilebilmektedir. Ancak ultra insansı olmayan(yani yapaylık seviyesi %10-15 civarında olan) yapısı nedeniyle, özellikle uzun cümlelerde robotik ton sezilebilmektedir. Özel telaffuz ayarları ve optimizasyonları yapılamasada buna rağmen her türlü metni konuşmaya çevirebilmektedir. Lisans olarak CC-BY-NC 4.0 kapsamında olduğu için kullanıldığı yazılımlarda özellikle Meta/Facebook’a atıfta bulunulması ve sadece kişisel, akademik veya araştırma amaçlı kullanımların dışında ticari kullanılmaması gerekmektedir. Yani ticari kullanım için özel lisans veya izin gerekebilmektedir.
Bu modeli aşağıdaki gibi kullanarak client’tan gelecek olan metinsel içeriğin rahatlıkla ses kaydını elde edebiliriz;
record TextToSpeechRequest(string Text);
app.MapPost("/text-to-speech", async (IHttpClientFactory httpClientFactory, HttpContext httpContext) =>
{
try
{
HttpClient httpClient = httpClientFactory.CreateClient("HuggingFaceAPI");
using StreamReader streamReader = new StreamReader(httpContext.Request.Body);
string requestBody = await streamReader.ReadToEndAsync();
TextToSpeechRequest request = JsonSerializer.Deserialize<TextToSpeechRequest>(requestBody)!;
var requestData = new
{
inputs = request.Text
};
StringContent stringContent = new(
JsonSerializer.Serialize(requestData),
Encoding.UTF8,
MediaTypeNames.Application.Json
);
Console.WriteLine("Ses oluşturuluyor...");
HttpResponseMessage response = await httpClient.PostAsync("models/facebook/mms-tts-tur", stringContent);
if (response.IsSuccessStatusCode)
{
byte[] audioBytes = await response.Content.ReadAsByteArrayAsync();
string outputPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "audio_outputs", $"output_{DateTime.Now:yyyyMMddHHmmss}.wav");
//Ses dosyası kaydediliyor
File.WriteAllBytes(outputPath, audioBytes);
httpContext.Response.ContentType = "audio/wav";
await httpContext.Response.Body.WriteAsync(audioBytes);
}
else
{
string error = await response.Content.ReadAsStringAsync();
Console.WriteLine($"API Hatası\t: {response.StatusCode}");
Console.WriteLine($"Hata Detayı\t: {error}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Hata oluştu\t: {ex.Message}");
}
});
Dikkat ederseniz; /text-to-speech endpoint’ini oluşturarak, 5. satırda biraz önce yapılandırdığımız ‘HuggingFaceAPI‘ adında HttpClient nesnesinden talep edip, oluşturuyoruz. Devamında client’tan gelen metni TextToSpeechRequest türünde yakalayıp, bunu 24. satırda ilgili modelin API’sine post isteğiyle gönderiyoruz. Eğer bu istek neticesi başarılıysa AI modeli bizlere gönderilen metinin ses dosyasını döndürecektir. 27. satırda bu ses dosyasını alıyoruz ve sonrasında belirtilen dizine atıyoruz.
İşte bu kadar 🙂
.NET’te Sppech-to-Text Uygulaması
Speech-to-Text için de OpenAI tarafından geliştirilen ve Automatic Speech Recognition (ASR) ile konuşma çevirisi görevlerinde kullanılan openai/whisper-large-v3-turbo AI modelini kullanacağız. MIT lisansıyla sunulduğu için ticari ya da değil tüm projelerde rahatlıkla kullanılabilmektedir. Ayrıca ücretsizdir. whisper-large-v3-turbo, önceki large-v3 modeline kıyasla optimize edilerek 8 kat daha hızlı çalışmakta ve buna rağmen transkripsiyon kalitesinde önemsiz bir düzeyde düşüşle ciddi performans artışı sağlamaktadır. Tüm bunların yanında 99 farklı dil desteğiyle geniş bir kullanım yelpazesi sunmakta ve genellikle canlı yayınlarda, anlık çevirilerde ve daha az donanım kaynağı gerektiren yüksek performans gerektiren süreçlerde tercih edilmektedir.
Bu modeli de aşağıdaki gibi işleyerek hazır olan ya da mikrofondan elde edilen ses dosyalarını metinsel ifadeye dönüştürebiliriz;
app.MapPost("/speech-to-text", async (IHttpClientFactory httpClientFactory, HttpContext httpContext) =>
{
try
{
IFormCollection form = await httpContext.Request.ReadFormAsync();
IFormFile? file = form.Files.GetFile("audio");
if (file is { Length: 0 } or null)
{
httpContext.Response.StatusCode = 400;
await httpContext.Response.WriteAsJsonAsync(new { Error = "Ses dosyası bulunamadı" });
return;
}
using MemoryStream memoryStream = new();
await file.CopyToAsync(memoryStream);
byte[] audioBytes = memoryStream.ToArray();
HttpClient httpClient = httpClientFactory.CreateClient("HuggingFaceAPI");
ByteArrayContent byteArrayContent = new(audioBytes);
byteArrayContent.Headers.Add(HeaderNames.ContentType, "audio/wav");
//Whisper small modeli kullanılıyor | Hızlı ve güvenilir!
HttpResponseMessage response = await httpClient.PostAsync("models/openai/whisper-large-v3-turbo", byteArrayContent);
string responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"API Yanıtı\t: {responseContent}");
if (!response.IsSuccessStatusCode)
{
httpContext.Response.StatusCode = (int)response.StatusCode;
await httpContext.Response.WriteAsJsonAsync(new { Error = $"API Hatası\t:{responseContent}" });
return;
}
//Whisper modeli için özel yanıt formatı ayarlanıyor
Dictionary<string, string>? result = JsonSerializer.Deserialize<Dictionary<string, string>>(responseContent);
string? text = result?.GetValueOrDefault("text", "");
if (string.IsNullOrEmpty(text))
{
httpContext.Response.StatusCode = 500;
await httpContext.Response.WriteAsJsonAsync(new { Error = "Metin çevrilemedi!" });
return;
}
await httpContext.Response.WriteAsJsonAsync(new { Text = text });
}
catch (Exception ex)
{
Console.WriteLine($"Hata\t: {ex.Message}");
httpContext.Response.StatusCode = 500;
await httpContext.Response.WriteAsJsonAsync(new { Error = $"Sunucu hatası\t: {ex.Message}" });
}
})
.DisableAntiforgery();
Yukarıdaki kod bloğuna göz atarsanız eğer; 5 ile 22. satır aralığında gelen ses dosyasını ByteArrayContent türünde elde ediyoruz ve bu veriyi 25. satırda AI modeline göndererek handle edilmesini başlatıyoruz. Devamında ise gelen text verisini ayıklayıp, response olarak döndürüyoruz.
Ve evet… Bu işlem de bu kadar 🙂
Şimdi gelin bu işlevsellikleri görselleştirerek kullanmamızı sağlayacak olan client’ın geliştirmesini gerçekleştirelim.
Client Tasarımı ve Geliştirilmesi
Öncelikle aşağıdaki gibi estetik ve romantizm anlayışına dayanan bir tasarımda bulunalım;
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ses-Metin Dönüştürücü</title>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<div class="tab-container">
<h1 style="text-align: center; margin-bottom: 20px;">Ses Dönüştürücü</h1>
<div class="tab-buttons">
<button onclick="showTab('tts')" class="tab-button active">Metin-Ses</button>
<button onclick="showTab('stt')" class="tab-button">Ses-Metin</button>
</div>
</div>
<div id="tts" class="tab-content container active">
<h2>Metin-Ses Dönüştürücü</h2>
<textarea id="inputText" placeholder="Türkçe metin girin..."></textarea>
<button onclick="convertToSpeech()">Sese Dönüştür</button>
<div id="ttsStatus" class="status"></div>
<audio id="audioPlayer" controls style="display: none; margin-top: 20px; width: 100%;"></audio>
</div>
<div id="stt" class="tab-content container">
<h2>Ses-Metin Dönüştürücü</h2>
<div id="audioControls">
<button id="recordButton" onclick="toggleRecording()">Kayıt Başlat</button>
<input type="file" id="audioFile" accept="audio/*" style="display: none;">
<button onclick="document.getElementById('audioFile').click()">Ses Dosyası Seç</button>
</div>
<div id="sttStatus" class="status"></div>
<textarea id="outputText" placeholder="Dönüştürülen metin burada görünecek..." readonly></textarea>
</div>
<script src="script.js"></script>
</body>
</html>
Bu tasarımda kullandığımız style.css dosyasının içeriğini haddinden fazla uzun olduğu için burada sunmayacağım. Amma velakin script.js dosyasının içeriğini masaya yatıracağız.
let mediaRecorder;
let audioChunks = [];
let isRecording = false;
function showTab(tabId) {
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
document.querySelector(`button[onclick="showTab('${tabId}')"]`).classList.add('active');
}
async function convertToSpeech() {
const inputText = document.getElementById('inputText').value;
const statusDiv = document.getElementById('ttsStatus');
const audioPlayer = document.getElementById('audioPlayer');
const button = document.querySelector('#tts button');
if (!inputText) {
statusDiv.className = 'status error';
statusDiv.textContent = 'Lütfen bir metin girin.';
return;
}
try {
button.disabled = true;
statusDiv.className = 'status';
statusDiv.textContent = 'Ses oluşturuluyor...';
const response = await fetch('/text-to-speech', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ Text: inputText })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
audioPlayer.src = audioUrl;
audioPlayer.style.display = 'block';
await audioPlayer.play();
statusDiv.className = 'status success';
statusDiv.textContent = 'Ses başarıyla oluşturuldu!';
} catch (error) {
statusDiv.className = 'status error';
statusDiv.textContent = `Hata oluştu: ${error.message}`;
console.error('Error:', error);
} finally {
button.disabled = false;
}
}
async function toggleRecording() {
const recordButton = document.getElementById('recordButton');
const statusDiv = document.getElementById('sttStatus');
if (!isRecording) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
await convertSpeechToText(audioBlob);
};
mediaRecorder.start();
isRecording = true;
recordButton.textContent = 'Kayıt Durdur';
recordButton.classList.add('recording');
statusDiv.className = 'status success';
statusDiv.textContent = 'Kayıt yapılıyor...';
} catch (error) {
statusDiv.className = 'status error';
statusDiv.textContent = `Mikrofon erişim hatası: ${error.message}`;
}
} else {
mediaRecorder.stop();
isRecording = false;
recordButton.textContent = 'Kayıt Başlat';
recordButton.classList.remove('recording');
}
}
document.getElementById('audioFile').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
await convertSpeechToText(file);
}
});
async function convertSpeechToText(audioData) {
const statusDiv = document.getElementById('sttStatus');
const outputText = document.getElementById('outputText');
try {
statusDiv.className = 'status';
statusDiv.textContent = 'Ses metne dönüştürülüyor...';
const formData = new FormData();
formData.append('audio', audioData);
const response = await fetch('/speech-to-text', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
outputText.value = result.text;
statusDiv.className = 'status success';
statusDiv.textContent = 'Ses başarıyla metne dönüştürüldü!';
} catch (error) {
statusDiv.className = 'status error';
statusDiv.textContent = `Hata oluştu: ${error.message}`;
console.error('Error:', error);
}
}
Evet, burada aşağıdaki görselde olduğu gibi işlevsel olarak mevzu bahis olan her iki dönüşümü gerçekleştirecek nitelikte bir tasarımla arayüz hazırlanmıştır.
Bu client, hem html hem de script olarak wwwroot dizinine eklendiği taktirde, uygulamanın temel yapılandırmasında bulunduğumuz UseDefaultFiles middleware’i sayesinde direkt uygulama ayağa kaldırılır kaldırılmaz açılacaktır. Böylece testlerimizi hızlıca gerçekleştirebilmekte ve TTS ve STT dönüşümlerinin orta şeker de olsa başarıyla gerçekleştiğini gözlemleyebilmekteyiz.
Evet… Nihai olarak, bu içeriğimizde Text-to-Speech (TTS) ve Speech-to-Text (STT) yaklaşımlarının Hugging Face üzerinden ilgili modeller eşliğinde API aracılığıyla basit bir şekilde entegrasyonlarını ele almış bulunuyoruz.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Not : Örnek çalışmaya aşağıdaki GitHub adresinden erişebilirsiniz.
https://github.com/gncyyldz/AI_TextToSpeech_And_SpeechToText_Example
