Skip to main content

shuyi_assets/voice/
plan.rs

1//! What a voice tool has to do for one language, as a pure function of the
2//! dialogue, the scenes to voice, the cast and the manifest's clips: the
3//! clips to make or make again (and why), the recorded clips a person must
4//! record again, the clips to remove and the lines nobody is cast to say.
5//! The tool reads and writes its files; [`plan`] only says what to do.
6//!
7//! The tool brings three hooks:
8//!
9//! - which variants a line needs, from its speaker and text (none for a
10//!   line nobody says aloud, such as a reply);
11//! - the words a line says as a variant (the hook
12//!   [`VoiceManifest::is_stale`](super::VoiceManifest::is_stale) takes);
13//! - whether the voice of a generated clip changed, in the tool's own
14//!   words ("the sample changed"). The plan itself compares the clip's
15//!   [`voice`](VoiceClip::voice) with the cast's.
16use crate::prelude::*;
17
18use alloc::collections::{BTreeMap, BTreeSet};
19
20use super::{Cast, CastVoice, ClipVariant, MadeBy, VoiceClip, clip_file};
21use crate::dialogue::{DialogueTable, LineId};
22
23/// Why a clip is to be made, or recorded, again.
24#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum Why {
27    /// The line has no clip.
28    Missing,
29    /// The clip says other words than the line says today.
30    Words,
31    /// The line's direction changed.
32    Direction,
33    /// The cast names another voice than the one that made the clip.
34    Recast,
35    /// The tool says the voice changed, and why, in its own words.
36    Voice(String),
37}
38
39impl Why {
40    /// What a tool's report says of it.
41    pub fn says(&self) -> &str {
42        match self {
43            Self::Missing => "no clip",
44            Self::Words => "the words changed",
45            Self::Direction => "the direction changed",
46            Self::Recast => "the cast names another voice",
47            Self::Voice(reason) => reason,
48        }
49    }
50}
51
52/// A clip a voiced line needs, with what the voice needs to say it.
53#[derive(Debug, Clone, PartialEq, Eq)]
54#[non_exhaustive]
55pub struct WantedClip<V> {
56    /// The line.
57    pub line: LineId,
58    /// Which of its clips.
59    pub variant: V,
60    /// Who says it: a character id, or the narration speaker.
61    pub speaker: String,
62    /// The cast key whose voice says it: the speaker and the variant's
63    /// suffix if the cast has that key, else the speaker.
64    pub key: String,
65    /// The words it says (what the manifest's `spoken` will be).
66    pub spoken: String,
67    /// How it is said.
68    pub direction: Option<String>,
69    /// The line before it in the scene, for context: its speaker and
70    /// words.
71    pub before: Option<(String, String)>,
72}
73
74impl<V: ClipVariant> WantedClip<V> {
75    /// Its file, relative to the language's folder ([`clip_file`]).
76    pub fn file(&self) -> String {
77        clip_file(&self.line, self.variant)
78    }
79}
80
81/// A clip a voiced line needs whose speaker the cast gives no voice.
82#[derive(Debug, Clone, PartialEq, Eq)]
83#[non_exhaustive]
84pub struct Uncast<V> {
85    /// The line.
86    pub line: LineId,
87    /// Which of its clips.
88    pub variant: V,
89    /// Who says it.
90    pub speaker: String,
91}
92
93/// Everything there is to do for one language's clips.
94#[derive(Debug, Clone, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct VoicePlan<V> {
97    /// Every clip the voiced lines need and the cast can say, by scene id,
98    /// then in script order.
99    pub wanted: Vec<WantedClip<V>>,
100    /// The generated (or missing) clips of [`wanted`](Self::wanted) to
101    /// make, by index.
102    pub make: Vec<(usize, Why)>,
103    /// The recorded clips of [`wanted`](Self::wanted) a person must record
104    /// again, by index: judged by their words and direction only.
105    pub record: Vec<(usize, Why)>,
106    /// Clips to remove: their line is gone, or their line, in a voiced
107    /// scene, needs other variants. A clip of a scene that isn't voiced
108    /// stays while its line exists.
109    pub remove: Vec<VoiceClip<V>>,
110    /// The clips voiced lines need whose speaker has no voice in the cast.
111    pub uncast: Vec<Uncast<V>>,
112}
113
114impl<V> Default for VoicePlan<V> {
115    fn default() -> Self {
116        Self {
117            wanted: Vec::new(),
118            make: Vec::new(),
119            record: Vec::new(),
120            remove: Vec::new(),
121            uncast: Vec::new(),
122        }
123    }
124}
125
126/// Plans `dialogue`'s clips: the lines of the scenes in `voiced` need a
127/// clip for each variant `variants(speaker, text)` names, saying
128/// `spoken(text, variant)` (a variant whose words are `None` needs none).
129/// `clips` are the manifest's. A generated clip whose voice is still the
130/// cast's is made again if `voice_changed(clip, cast voice)` gives a
131/// reason.
132pub fn plan<V: ClipVariant, S, D>(
133    dialogue: &DialogueTable<D>,
134    voiced: &BTreeSet<String>,
135    cast: &Cast<S>,
136    clips: &[VoiceClip<V>],
137    variants: impl Fn(&str, &str) -> &'static [V],
138    spoken: impl Fn(&str, V) -> Option<String>,
139    voice_changed: impl Fn(&VoiceClip<V>, &CastVoice<S>) -> Option<String>,
140) -> VoicePlan<V> {
141    let mut plan = VoicePlan::default();
142    let mut needed = BTreeSet::new();
143    let mut in_voiced = BTreeSet::new();
144    let mut all = BTreeSet::new();
145    for (id, scene) in &dialogue.scenes {
146        let lines = scene.lines();
147        all.extend(lines.iter().map(|l| l.id.clone()));
148        if !voiced.contains(id) {
149            continue;
150        }
151        let mut before = None;
152        for line in lines {
153            in_voiced.insert(line.id.clone());
154            for &variant in variants(line.speaker, line.text) {
155                let Some(words) = spoken(line.text, variant) else {
156                    continue;
157                };
158                needed.insert((line.id.clone(), variant));
159                let Some(key) = key_for(cast, line.speaker, variant) else {
160                    plan.uncast.push(Uncast {
161                        line: line.id.clone(),
162                        variant,
163                        speaker: line.speaker.to_owned(),
164                    });
165                    continue;
166                };
167                plan.wanted.push(WantedClip {
168                    line: line.id.clone(),
169                    variant,
170                    speaker: line.speaker.to_owned(),
171                    key,
172                    spoken: words,
173                    direction: line.direction.map(str::to_owned),
174                    before: before.clone(),
175                });
176            }
177            let shown = V::variants()
178                .into_iter()
179                .find_map(|v| spoken(line.text, v))
180                .unwrap_or_else(|| line.text.to_owned());
181            before = Some((line.speaker.to_owned(), shown));
182        }
183    }
184    let made: BTreeMap<(&LineId, V), &VoiceClip<V>> = clips
185        .iter()
186        .map(|clip| ((&clip.line, clip.variant), clip))
187        .collect();
188    for (i, wanted) in plan.wanted.iter().enumerate() {
189        let Some(&clip) = made.get(&(&wanted.line, wanted.variant)) else {
190            plan.make.push((i, Why::Missing));
191            continue;
192        };
193        let voice = &cast.voices[&wanted.key];
194        if let Some(why) = why_again(clip, wanted, voice, &voice_changed) {
195            match clip.made_by {
196                MadeBy::Recorded { .. } => plan.record.push((i, why)),
197                MadeBy::Generated { .. } => plan.make.push((i, why)),
198            }
199        }
200    }
201    plan.remove = clips
202        .iter()
203        .filter(|clip| {
204            let ours = in_voiced.contains(&clip.line) || !all.contains(&clip.line);
205            ours && !needed.contains(&(clip.line.clone(), clip.variant))
206        })
207        .cloned()
208        .collect();
209    plan
210}
211
212/// The cast key whose voice says `speaker`'s clips of `variant`, as
213/// [`Cast::voice_for`] picks it.
214fn key_for<S, V: ClipVariant>(cast: &Cast<S>, speaker: &str, variant: V) -> Option<String> {
215    let own = format!("{speaker}{}", variant.suffix());
216    [own.as_str(), speaker]
217        .into_iter()
218        .find(|key| cast.voices.contains_key(*key))
219        .map(str::to_owned)
220}
221
222/// Why `clip` must be made, or recorded, again as `wanted` with the cast's
223/// `voice`, if it must. A recorded clip is judged by its words and
224/// direction only.
225fn why_again<V, S>(
226    clip: &VoiceClip<V>,
227    wanted: &WantedClip<V>,
228    voice: &CastVoice<S>,
229    voice_changed: &impl Fn(&VoiceClip<V>, &CastVoice<S>) -> Option<String>,
230) -> Option<Why> {
231    if clip.spoken != wanted.spoken {
232        Some(Why::Words)
233    } else if clip.direction != wanted.direction {
234        Some(Why::Direction)
235    } else if matches!(clip.made_by, MadeBy::Recorded { .. }) {
236        None
237    } else if clip.voice != voice.voice {
238        Some(Why::Recast)
239    } else {
240        voice_changed(clip, voice).map(Why::Voice)
241    }
242}
243
244/// The clips of `plan` the voice under cast key `key` says, in the plan's
245/// order (by scene, then script order): what that voice records in a
246/// session, each with the line before it.
247pub fn recording_script<'a, V>(plan: &'a VoicePlan<V>, key: &str) -> Vec<&'a WantedClip<V>> {
248    plan.wanted.iter().filter(|w| w.key == key).collect()
249}
250
251#[cfg(test)]
252mod tests;