port: the runtime now says what the voice export is missing, at the moment it plays it

The manifest has carried the gap for weeks and the runtime printed '+ voice ADV'
and nothing else. A reader of manifest.json gets a paragraph; a person LISTENING
gets clean dialogue and no way to learn a stream is absent. NEW GAME already
announces the screens it jumps over; audio had no equivalent.

ManifestAudio gains  -- one line naming what is KNOWN missing, absent
meaning nothing is known rather than nothing is wrong -- and MenuAudio carries it
so _play_video can print it. Verified on the boot's ADV and P7's S00A.

The first version of the message was FALSE for one of the two assets: it said
'one is a start-truncated stream', which is ADV's story, where S00A's dropped
chunks are digitally silent. Caught by reading the output for both, which I
nearly skipped because the ADV line was obviously right. Now states the counts
and points at the entry's why.

A message generated once from a template but true only for the case it was
written against is harder to see than a wrong number -- the sentence is
well-formed and confident in both places.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
Sylpheed port agent
2026-08-30 08:49:25 +00:00
parent 507155422b
commit 64b76b0892
4 changed files with 90 additions and 1 deletions

View File

@@ -95,6 +95,18 @@ struct ManifestAudio {
peak_dbfs: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
duration_s: Option<f32>,
/// 🔴 One line saying what this asset is KNOWN to be missing, for the
/// runtime to announce. Absent means nothing is known to be missing --
/// never that the asset was checked and is complete.
///
/// It exists because the export could already say this and the RUNTIME
/// could not. `why` carries the full account, but it is a paragraph aimed
/// at a reader of the manifest; a player hears clean dialogue and has no
/// way to learn that a stream is absent from it. This port already
/// announces the two measured screens NEW GAME jumps over, on the principle
/// that a gap is announced before it is opened. Audio had no equivalent.
#[serde(skip_serializing_if = "Option::is_none")]
incomplete: Option<String>,
/// The game's own cue identifier where one is a NAME MATCH. Absent means
/// nobody has claimed one -- never that the binding is unknown.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -505,6 +517,15 @@ impl From<audio::Exported> for ManifestAudio {
why: a.why,
peak_dbfs: a.peak_dbfs,
duration_s: a.duration_s,
incomplete: (a.kept_waves < a.sub_waves).then(|| {
format!(
"{} of {} streams. The running game decodes all {} concurrently. \
Nothing in the audio reveals the gap -- what plays is clean dialogue. \
WHICH streams are dropped and why differs per asset; the manifest \
entry's `why` says, and it is not the same story twice.",
a.kept_waves, a.sub_waves, a.sub_waves
)
}),
name_match: a.name_match,
loop_mode: a.loop_mode,
}

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
128 sections. Search this before re-deriving anything.
129 sections. Search this before re-deriving anything.
* [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28)
* [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28)
@@ -139,6 +139,7 @@ dies, which is what this file is for.
* [The voice export now carries every qualifying stream — and a unity sum was refused by our own check](#the-voice-export-now-carries-every-qualifying-stream--and-a-unity-sum-was-refused-by-our-own-check)
* [Their stream assignment does not fit my region — weights NOT applied](#their-stream-assignment-does-not-fit-my-region--weights-not-applied)
* [The resolver starts late, and my "duplicate tail" was a real stream all along](#the-resolver-starts-late-and-my-duplicate-tail-was-a-real-stream-all-along)
* [The export knew the voice was incomplete; the runtime did not say so](#the-export-knew-the-voice-was-incomplete-the-runtime-did-not-say-so)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -7498,3 +7499,46 @@ not have:
✅ The assignment itself still stands — their ratio test was chosen to be immune to
the clipping — but ⚠️ chunk 0's absolute level was measured over 62 % of its
stream, so its 0.05 dB agreement is luckier than it reads.
## The export knew the voice was incomplete; the runtime did not say so
The manifest has carried a full account of the voice export's gap for weeks, and
the runtime printed `+ voice ADV` and nothing else. That asymmetry is the
dangerous one for audio specifically: a reader of `manifest.json` gets a
paragraph, and a person **listening** gets clean dialogue with no way to learn
that a stream is absent from it.
This port already governs the same situation elsewhere — NEW GAME announces the
two measured screens it jumps over rather than skipping them silently. Audio had
no equivalent, so:
* `ManifestAudio` gains **`incomplete`**, one line naming what an asset is *known*
to be missing. Absent means nothing is known to be missing — **never** that the
asset was checked and found complete.
* `MenuAudio` carries it alongside the stream, and `_play_video` prints it at the
moment the voice starts.
```
+ voice ADV
🔴 KNOWN INCOMPLETE: 2 of 3 streams. The running game decodes all 3 …
```
Verified on both paths — the boot's `ADV` and P7's `S00A`.
### 🔴 And the first version of the message was false for one of the two assets
It read *"one is a start-truncated stream this export drops"*. That is `ADV`'s
story: its dropped chunk is the start-clipped remainder of ctx0. **`S00A`'s
dropped chunks are digitally silent** — a different reason entirely — and the
message would have told anyone running the new-game intro something untrue about
their own asset.
Caught by reading the output for both, which took one command and which I nearly
skipped because the `ADV` line was obviously correct. The wording now states the
counts and points at the entry's `why`, because **which streams are dropped and
why is not the same story twice** and a single sentence cannot carry both.
⚠️ Worth naming as its own shape: a message generated *once* from a template but
*true* only for the case it was written against. It is the failure mode of every
generic warning, and it is harder to see than a wrong number because the sentence
is well-formed and confident in both places.

View File

@@ -543,6 +543,17 @@ func _play_video(name: String, skippable: bool) -> void:
# would be authoring a sync constant nobody measured.
if audio.play_voice(name):
print(" + voice %s" % name)
# 🔴 SAY WHAT IS MISSING, at the moment it is played.
#
# The manifest has known the voice export is incomplete for weeks and the
# runtime did not repeat it. That asymmetry is the dangerous one for
# audio: a reader of `manifest.json` gets a paragraph, and a person
# LISTENING gets clean dialogue with no way to learn a stream is absent.
# The same principle already governs NEW GAME, which announces the two
# measured screens it jumps over rather than skipping them silently.
var gap := audio.incomplete_for(name)
if gap != "":
print(" 🔴 KNOWN INCOMPLETE: %s" % gap)
else:
# Said out loud: silence is the audio failure that looks like success,
# and "this cutscene is unvoiced" is a real answer for most of the disc.

View File

@@ -24,6 +24,14 @@ extends Node
var cues: Dictionary = {}
## Role -> {stream, loop}, from the entries of kind `bgm`.
var beds: Dictionary = {}
## Movie name -> what that voice export is KNOWN to be missing, from the
## manifest's `incomplete`. Empty for an asset with no known gap.
var _voice_gaps: Dictionary = {}
## What `movie`'s voice export is known to be missing, or "" if nothing is.
func incomplete_for(movie: String) -> String:
return String(_voice_gaps.get(movie, ""))
## Movie name -> stream, from the entries of kind `voice`.
##
## A cutscene's dialogue is NOT in its `.ogv`. On this disc a movie carries music
@@ -86,6 +94,11 @@ func configure(tree: ExportTree) -> bool:
# authored about which recording belongs to which picture.
stream.loop = false
voices[String(entry["name"])] = stream
# Carried alongside the stream so the runtime can announce a known gap
# at the moment it plays one. Absent means nothing is KNOWN to be
# missing -- never that the asset was checked and is complete.
if entry.has("incomplete"):
_voice_gaps[String(entry["name"])] = String(entry["incomplete"])
_:
push_warning("manifest audio entry %s has kind %s, which this build does not play"
% [entry.get("name", "?"), entry.get("kind", "?")])