StreamPartial.java

package com.shengzhiai.yugu.model;

import com.fasterxml.jackson.databind.JsonNode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Intermediate frame {@code {"eof":0,"result":{...}}} of a stream that requested realtime feedback
 * ({@link EvaluateConfig#realtimeFeedback(boolean)} or {@link CompatConfig#realtimeFeedback(boolean)}),
 * delivered through {@code StreamListener.onPartial} on both channels.
 *
 * <p>When the platform tracks the reading progress of the round, {@code result.progress} lists every unit
 * of the reference text in order ({@link #getProgress()}) next to the three counts:
 * <pre>{@code
 * {"eof":0,"result":{"bytes":96000,"readUnits":19,"matchedUnits":18,"totalUnits":25,
 *  "progress":[{"word":"我","read":true,"matched":true}, ...]}}
 * }</pre>
 * Otherwise (realtime feedback off, empty reference text, audio that is not 16 kHz 16 bit mono PCM,
 * tracking unavailable) the frame is {@code {"eof":0,"result":{"bytes":n}}}: {@link #hasProgress()} is
 * false, the counts are -1 and the progress list is empty.
 *
 * <p>Compat frames follow the Shengtong {@code realtime_feedback} intermediate result and also carry
 * {@code "recordId":""}, {@code result.kernel_version}, {@code result.resource_version} and the
 * Shengtong-shaped incremental {@code result.words} (the units read since the previous frame). The SDK
 * does not map those fields; they stay available through {@link #getRaw()}.
 */
public final class StreamPartial {
    private final long bytes;
    private final JsonNode raw;
    private final int readUnits;
    private final int matchedUnits;
    private final int totalUnits;
    private final List<ReadingWord> progress;

    /**
     * @param bytes audio bytes the server has received in this server session
     * @param raw   the frame; the reading progress is read from {@code result.progress} and the counts
     */
    public StreamPartial(long bytes, JsonNode raw) {
        this.bytes = bytes;
        this.raw = raw;
        JsonNode result = raw == null ? null : raw.get("result");
        this.readUnits = count(result, "readUnits");
        this.matchedUnits = count(result, "matchedUnits");
        this.totalUnits = count(result, "totalUnits");
        JsonNode list = result == null ? null : result.get("progress");
        if (list != null && list.isArray()) {
            List<ReadingWord> units = new ArrayList<>(list.size());
            for (JsonNode u : list) {
                units.add(new ReadingWord(u.path("word").asText(""), u.path("read").asBoolean(false),
                        u.path("matched").asBoolean(false)));
            }
            this.progress = Collections.unmodifiableList(units);
        } else {
            this.progress = null;
        }
    }

    private static int count(JsonNode result, String field) {
        JsonNode n = result == null ? null : result.get(field);
        return n != null && n.isIntegralNumber() && n.canConvertToInt() ? n.intValue() : -1;
    }

    /** @return audio bytes received by the server session so far, -1 when the frame carried none */
    public long getBytes() {
        return bytes;
    }

    /** @return the raw frame, including the compat fields the SDK does not map */
    public JsonNode getRaw() {
        return raw;
    }

    /**
     * @return true when the frame carries the reading progress ({@code result.progress} present)
     * @since 2.1.0
     */
    public boolean hasProgress() {
        return progress != null;
    }

    /**
     * @return number of units whose {@code read} is true, the reading position; -1 when absent as in
     *         frames without progress
     * @since 2.1.0
     */
    public int getReadUnits() {
        return readUnits;
    }

    /**
     * @return number of units whose {@code matched} is true, -1 when absent as in frames without progress
     * @since 2.1.0
     */
    public int getMatchedUnits() {
        return matchedUnits;
    }

    /**
     * @return number of units of the reference text, -1 when absent as in frames without progress
     * @since 2.1.0
     */
    public int getTotalUnits() {
        return totalUnits;
    }

    /**
     * Reading state of every unit of the reference text in order ({@code result.progress}): characters
     * for Chinese, words for English.
     *
     * @return unmodifiable list, empty when the frame carries no progress
     * @since 2.1.0
     */
    public List<ReadingWord> getProgress() {
        return progress == null ? Collections.emptyList() : progress;
    }

    @Override
    public String toString() {
        if (progress == null) {
            return "StreamPartial{bytes=" + bytes + '}';
        }
        return "StreamPartial{bytes=" + bytes + ", readUnits=" + readUnits + ", matchedUnits=" + matchedUnits
                + ", totalUnits=" + totalUnits + '}';
    }
}