Spring AI · Səs və şəkillərlə iş · Dərs 2
Mətndən nitqin yaradılması
Uyğun modellərlə səsi mətnə, mətni nitqə çevir. Şəkillər haqqında suallar ver və mətn sorğuları əsasında şəkillər yarat.
Mətndən nitqin yaradılması
Tələbə qısa izahı yoxladıqdan sonra Study Coach onu səsləndirə bilər. Nitq sintezi mətn qəbul edir və audio qaytarır. İzahın düzgünlüyünü yoxlamır; axıcı səs qeyri-müəyyən cavabı qəti həqiqət kimi göstərməməlidir.
1. Dinləmə seçimini istifadəçiyə ver
Mətn versiyası görünən qalsın və tələbə onu dinləmək istəyib-istəmədiyini seçsin. Real yaradılmış audio üçün pleyerdə “Süni intellektlə yaradılmış səs” işarəsini aydın göstər. Hazır sintetik səsi insan müəllim kimi təqdim etmə və müəyyən şəxsin onu yazdığını demə. Provayderin cari TTS təlimatı da bu məlumatın açıq göstərilməsini tələb edir. Provayderin nitq təlimatı
API-də model, səs, çıxış formatı və əlavə parametrlər ayrı seçimlərdir. Bir modeldə olan səs başqa provayderdə olmaya bilər. Dil dəstəyi Azərbaycan dilində texniki terminlərin düzgün tələffüzünə zəmanət vermir. Əlçatanlıq və ya keyfiyyət iddiasından əvvəl real dərsləri təmsil edən cümlələri yoxla.
Lokal stub qısa səssiz WAV qaytarır. Nitq yaratmadan, hesabdan ödəniş çıxmadan və dərs mətnini xarici xidmətə göndərmədən ikili cavab yolunu yoxlayır.
2. Mətn-səs API-sindən istifadə et
Ayrıca media laboratoriyasını davam etdir. src/main/java/academy/study/media/StudySpeech.java faylını yarat:
package academy.study.media;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import org.springframework.ai.audio.tts.TextToSpeechPrompt;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import org.springframework.ai.openai.OpenAiAudioSpeechOptions;
import org.springframework.ai.openai.api.OpenAiAudioApi;
public final class StudySpeech {
private final OpenAiAudioSpeechModel model;
public StudySpeech(URI localStub) {
var options = OpenAiAudioSpeechOptions.builder().model("lab-speech").voice("alloy")
.responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.WAV).speed(1.0).build();
model = new OpenAiAudioSpeechModel(LocalMediaTransport.audio(localStub), options,
LocalMediaTransport.once());
}
public byte[] speakReviewedText(String reviewedText) throws IOException {
String text = MediaFiles.text(reviewedText, 1_000);
var response = model.call(new TextToSpeechPrompt(text));
if (response == null || response.getResult() == null || response.getResult().getOutput() == null) {
throw new IllegalStateException("Speech unavailable");
}
return MediaFiles.wav(new ByteArrayInputStream(response.getResult().getOutput()));
}
}Spring AI 1.1.8-də sorğu tipi org.springframework.ai.audio.tts paketindəki TextToSpeechPrompt-dur. Cavabın nəticəsi byte[] saxlayır. Qaytarılan baytları MP3 fərz etmək əvəzinə WAV formatını və səsi açıq seçirik. Kod streaming audio deyil, bloklayan çağırış istifadə edir. Spring AI nitq API-si, nitq modelinin sabit versiyası
lab-speech uydurma stub modelidir. alloy sorğuda seçilən səs adıdır, amma stub həmin səsi yaratmır. Uğurlu test real modelin bütün seçimləri qəbul etdiyini göstərmir. Provayderin bəzi yeni imkanları bu Spring AI adapterində olmaya bilər; onları açmazdan əvvəl hər iki tərəfi yoxla.
3. Mətnin nəzərdən keçirilməsini giriş yoxlamasından ayır
speakReviewedText sorğudan əvvəl null, boş və uzun mətni rədd edir. Adı çağıran kodun məsuliyyətini bildirir; insanın mətni yoxladığını sübut etmir. Real UI səsləndirmə seçimini ekranda göstərilən dəqiq mətnə bağlamalı, onu səssizcə yeni model cavabı ilə əvəz etməməlidir.
1,000 kod vahidlik giriş həddi bir çağırışda göndərilən mətnin miqdarını azaldır. On saniyəlik səs yazısına zəmanət vermir. Nitqin müddəti məzmundan, dildən və danışıq sürətindən asılıdır. Metod qaytarılan WAV-ı əvvəlki on saniyəlik audio qaydasına görə ayrıca yoxlayır. Buna görə uzun, amma düzgün yaradılmış nitq provayder ödənişli işi gördükdən sonra rədd edilə bilər. Real məhsulda daha qısa təsdiqlənmiş parçalar və ölçülmüş hədlər istifadə et.
LocalMediaTransport.once() bu laboratoriyada avtomatik təkrar cəhdləri söndürür. Timeout-dan sonra yalnız exception-a baxaraq nitqin uzaq tərəfdə tamamlandığını bilmək olmaz. Təkrar cəhd qaydası əlavə ödənişləri və provayderin real idempotentlik davranışını nəzərə almalıdır; istənilən request ID-nin yalnız bir ödənişli əməliyyata zəmanət verdiyini düşünmə.
4. Məlum audio formatını təhlükəsiz qaytar
Cavab MediaFiles.wav metodundan keçir: açılmış real format, kanal sayı, nümunə eni, tezlik, frame sayı və bayt ölçüsü laboratoriya müqaviləsinə uyğun olmalıdır. Təkcə audio/wav cavab başlığı kifayət deyil. Yenidən kodlama çağıran tərəfə yoxlanmamış provayder məzmunu əvəzinə standartlaşdırılmış WAV verir.
Bu yoxlamaya qədər adapter cavabın baytlarını artıq yaddaşa alıb. Çıxış yoxlaması şəbəkə buferi həddi deyil və decoder üçün hər cür zərərli girişi dayandırmır. Real gateway və codec izolasiyası həmin hədləri ayrıca təmin etməlidir.
Metod baytlar qaytarır, fayl sistemi yolu seçmir. Tətbiq onları saxlayırsa, idarə olunan qovluqda serverin yaratdığı addan istifadə et, mövcud faylı əvəz etmə və çıxışı təsdiqlənmiş tələbə hesabına bağla. Brauzer cavabında audio/wav, neytral fayl adı və şəxsi mətn üçün uyğun private-cache qaydası olmalıdır. Səsləndirmə uğursuz olduqda mətn qalsın; audio xətasını uydurma uğur mesajı ilə əvəz etmə.
5. Sorğunu və audio konteynerini yoxla
src/test/java/academy/study/media/StudySpeechTest.java faylını yarat:
package academy.study.media;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import javax.sound.sampled.AudioSystem;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class StudySpeechTest {
private static final String PATH = "/v1/audio/speech";
@Test
void sendsReviewedTextAndParsesSyntheticWav() throws Exception {
try (var stub = new MediaStub()) {
stub.reply(PATH, 200, "audio/wav", MediaStub.wav(24_000, 2_400));
byte[] audio = new StudySpeech(stub.base()).speakReviewedText("A class has one direct superclass.");
var json = stub.lastJson();
assertEquals("lab-speech", json.path("model").asText());
assertEquals("alloy", json.path("voice").asText());
assertEquals("wav", json.path("response_format").asText());
assertEquals("A class has one direct superclass.", json.path("input").asText());
try (var decoded = AudioSystem.getAudioInputStream(new ByteArrayInputStream(audio))) {
assertEquals(24_000, decoded.getFormat().getSampleRate());
assertEquals(2_400, decoded.getFrameLength());
}
assertEquals(1, stub.calls.get());
}
}
@Test
void invalidTextNeverReachesTheProvider() throws Exception {
try (var stub = new MediaStub()) {
var model = new StudySpeech(stub.base());
for (String text : new String[]{null, " ", "x".repeat(1_001)}) {
assertThrows(IllegalArgumentException.class, () -> model.speakReviewedText(text));
}
assertEquals(0, stub.calls.get());
}
}
@Test
void contentTypeDoesNotMakeErrorTextIntoAudio() throws Exception {
try (var stub = new MediaStub()) {
stub.reply(PATH, 200, "audio/wav", "not-audio".getBytes(StandardCharsets.UTF_8));
assertThrows(IllegalArgumentException.class, () -> new StudySpeech(stub.base()).speakReviewedText("Study Java."));
}
}
@Test
void outputByteAndDurationCapsAreEnforced() throws Exception {
for (byte[] output : new byte[][]{new byte[MediaFiles.MAX_WAV_BYTES + 1], MediaStub.wav(16_000, 160_001)}) {
try (var stub = new MediaStub()) {
stub.reply(PATH, 200, "audio/wav", output);
assertThrows(IllegalArgumentException.class, () -> new StudySpeech(stub.base()).speakReviewedText("Study Java."));
}
}
}
@Test
void failedSynthesisIsNotAutomaticallyRepeated() throws Exception {
try (var stub = new MediaStub()) {
stub.reply(PATH, 500, "text/plain", "private-synthesis-error".getBytes(StandardCharsets.UTF_8));
var failure = assertThrows(IllegalStateException.class,
() -> new StudySpeech(stub.base()).speakReviewedText("Study Java."));
assertEquals("Media request failed: HTTP 500", failure.getMessage());
assertEquals(1, stub.calls.get());
}
}
}Beş metod sorğunun mətn, model, səs və format sahələrini dəqiq yoxlayır, məlum səssiz WAV-ı açır, yanlış girişi HTTP-dən əvvəl rədd edir, aldadıcı content type-ı qəbul etmir, çıxış hədlərini tətbiq edir və server xətasında yalnız bir cəhd edildiyini göstərir. Test audiosu 24 kHz tezlikdə 2,400 frame saxlayır, yəni 0.1 saniyədir. Səs kimliyi, təbiiliyi və ya tələffüz barədə heç nə göstərmir.
6. Testi işə sal və uyğun nəticəni qiymətləndir
Nitq sinfini və testi əlavə etdikdən sonra işə sal:
mvn -B -ntp -Dtest=StudyTranscriptionTest,StudySpeechTest testBu laboratoriyada on beş uğurlu metod gözlənilir. Maven asılılıqları və plugin-ləri keşləndikdən sonra -o istifadə et. Provayder açarı oxunmur. Növbəti dərs şəkil testlərini əlavə edir.
Tapşırıq: əvvəlki nitq sorğusu davam edərkən tələbə görünən izahı dəyişir. Pleyer hansı mətni oxuduğunu bildirməlidir?
Cavabı yoxla: audio nəticəsini göndərilmiş dəqiq mətnə və ya onun versiyasına bağla. Köhnə nəticəni köhnəlmiş kimi işarələ və ya at. Onu yeni izahın səsi kimi göstərmə. Bu, tətbiq vəziyyətinin idarə olunmasıdır; nitq modeli həmin problemi həll edə bilməz.
Real istifadədən əvvəl texniki sözləri, rəqəmləri, qısaltmaları və əlçatanlıq idarəetməsini nəzərdə tutulan istifadəçilərlə yoxla. Məhdud nümunələrlə gecikmə və xərci ölç, silinmə və saxlanma davranışını sənədləşdir. Qısa yoxlanmış cümlə kifayət edirsə, şəxsi söhbət tarixçəsini göndərmə.