- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
139 lines
5.9 KiB
Go
139 lines
5.9 KiB
Go
package yandex
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
stt "github.com/yandex-cloud/go-genproto/yandex/cloud/ai/stt/v3"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// Сохранённый ответ провайдера — единственное, из чего пересчитывается архив:
|
|
// результат операции у SpeechKit не переспрашивается, и повторное распознавание
|
|
// стоит денег. Поэтому проверки ниже судят не «разбор чего-то вернул», а
|
|
// сохранность самого ответа.
|
|
|
|
// response собирает ответ потока с одной репликой.
|
|
func response(text string, start, end int64) *stt.StreamingResponse {
|
|
return &stt.StreamingResponse{
|
|
Event: &stt.StreamingResponse_FinalRefinement{
|
|
FinalRefinement: &stt.FinalRefinement{
|
|
Type: &stt.FinalRefinement_NormalizedText{
|
|
NormalizedText: &stt.AlternativeUpdate{
|
|
Alternatives: []*stt.Alternative{
|
|
{Text: text, StartTimeMs: start, EndTimeMs: end},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Сохранённое читается обратно тем же: реплики со временем и плоский текст.
|
|
func TestPayloadSurvivesRoundTrip(t *testing.T) {
|
|
responses := []*stt.StreamingResponse{
|
|
response("Первая реплика.", 0, 900),
|
|
response("Вторая реплика.", 900, 1800),
|
|
}
|
|
|
|
raw, err := encodeResponses(responses)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, raw)
|
|
|
|
decoded, err := decodeResponses(raw)
|
|
require.NoError(t, err)
|
|
require.Len(t, decoded, 2)
|
|
|
|
outcome := outcomeFromResponses(decoded)
|
|
require.Len(t, outcome.Replicas, 2)
|
|
assert.Equal(t, "Первая реплика.", outcome.Replicas[0].Text)
|
|
assert.Equal(t, int64(0), outcome.Replicas[0].StartMs)
|
|
assert.Equal(t, int64(900), outcome.Replicas[0].EndMs)
|
|
assert.Equal(t, "Вторая реплика.", outcome.Replicas[1].Text)
|
|
assert.Equal(t, "Первая реплика. Вторая реплика.", outcome.PlainText)
|
|
}
|
|
|
|
// Ради этого свойства сохранение и сделано двоичным. Провайдер добавляет поля
|
|
// без предупреждения, и текстовое представление, собранное по нашей
|
|
// скомпилированной схеме, выбросило бы их молча — а пересчитать архив было бы
|
|
// уже не из чего: операция не переспрашивается.
|
|
func TestUnknownProviderFieldSurvivesStorage(t *testing.T) {
|
|
original := response("Реплика.", 0, 500)
|
|
|
|
// Так выглядит поле, которого наша схема не знает: провайдер прислал его,
|
|
// разбор положил в неизвестные.
|
|
unknown := protoimplUnknown(t)
|
|
original.ProtoReflect().SetUnknown(unknown)
|
|
require.NotEmpty(t, original.ProtoReflect().GetUnknown(), "неизвестное поле поставлено")
|
|
|
|
raw, err := encodeResponses([]*stt.StreamingResponse{original})
|
|
require.NoError(t, err)
|
|
|
|
decoded, err := decodeResponses(raw)
|
|
require.NoError(t, err)
|
|
require.Len(t, decoded, 1)
|
|
|
|
assert.Equal(t, []byte(unknown), []byte(decoded[0].ProtoReflect().GetUnknown()),
|
|
"неизвестное провайдерское поле пережило запись и чтение")
|
|
|
|
// И известное при этом на месте.
|
|
outcome := outcomeFromResponses(decoded)
|
|
require.Len(t, outcome.Replicas, 1)
|
|
assert.Equal(t, "Реплика.", outcome.Replicas[0].Text)
|
|
}
|
|
|
|
// Обрезанное вложение узнаётся отказом, а не половиной расшифровки: половина
|
|
// текста, выданная за целую, тише и хуже отказа.
|
|
func TestTruncatedPayloadIsRefused(t *testing.T) {
|
|
raw, err := encodeResponses([]*stt.StreamingResponse{response("Реплика.", 0, 500)})
|
|
require.NoError(t, err)
|
|
require.Greater(t, len(raw), 2)
|
|
|
|
_, err = decodeResponses(raw[:len(raw)-2])
|
|
assert.Error(t, err, "обрезанное вложение не разбирается молча")
|
|
}
|
|
|
|
// Пустой поток даёт пустой результат, а не отказ: «на записи нет текста» —
|
|
// законный исход распознавания.
|
|
func TestEmptyStreamGivesEmptyOutcome(t *testing.T) {
|
|
raw, err := encodeResponses(nil)
|
|
require.NoError(t, err)
|
|
|
|
decoded, err := decodeResponses(raw)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, decoded)
|
|
|
|
outcome := outcomeFromResponses(decoded)
|
|
assert.Empty(t, outcome.Replicas)
|
|
assert.Empty(t, outcome.PlainText)
|
|
}
|
|
|
|
// Ответ без разбора текста реплик не даёт и разбор не роняет: провайдер шлёт по
|
|
// потоку и служебные события.
|
|
func TestResponseWithoutTextIsSkipped(t *testing.T) {
|
|
responses := []*stt.StreamingResponse{
|
|
{Event: &stt.StreamingResponse_FinalRefinement{FinalRefinement: &stt.FinalRefinement{}}},
|
|
response("Реплика.", 0, 500),
|
|
response("", 500, 600),
|
|
}
|
|
|
|
outcome := outcomeFromResponses(responses)
|
|
require.Len(t, outcome.Replicas, 1, "пустые и служебные события репликами не становятся")
|
|
assert.Equal(t, "Реплика.", outcome.PlainText)
|
|
}
|
|
|
|
// protoimplUnknown собирает байты неизвестного поля: номер поля, которого в
|
|
// нашей схеме нет, с целочисленным значением.
|
|
func protoimplUnknown(t *testing.T) []byte {
|
|
t.Helper()
|
|
|
|
// Поле 4095, тип varint, значение 7 — заведомо за пределами схемы ответа.
|
|
raw, err := proto.Marshal(&stt.StreamingResponse{})
|
|
require.NoError(t, err)
|
|
require.Empty(t, raw)
|
|
|
|
return []byte{0xF8, 0xFF, 0x3F, 0x07}
|
|
}
|