finance_query/models/transcript/
mod.rs

1//! Earnings call transcript models.
2//!
3//! Typed models for Yahoo Finance earnings call transcripts.
4
5use serde::{Deserialize, Serialize};
6
7/// Full transcript response from Yahoo Finance.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct Transcript {
11    /// The transcript content including speakers and paragraphs.
12    pub transcript_content: TranscriptContent,
13    /// Metadata about the transcript.
14    pub transcript_metadata: TranscriptMetadata,
15}
16
17impl Transcript {
18    /// Get the full transcript text.
19    pub fn text(&self) -> &str {
20        self.transcript_content
21            .transcript
22            .as_ref()
23            .map(|t| t.text.as_str())
24            .unwrap_or("")
25    }
26
27    /// Get the fiscal quarter (e.g., "Q4").
28    pub fn quarter(&self) -> &str {
29        &self.transcript_metadata.fiscal_period
30    }
31
32    /// Get the fiscal year.
33    pub fn year(&self) -> i32 {
34        self.transcript_metadata.fiscal_year
35    }
36
37    /// Get speaker name by speaker ID.
38    pub fn speaker_name(&self, speaker_id: i32) -> Option<&str> {
39        self.transcript_content
40            .speaker_mapping
41            .iter()
42            .find(|s| s.speaker == speaker_id)
43            .map(|s| s.speaker_data.name.as_str())
44    }
45
46    /// Get all paragraphs with speaker names resolved.
47    pub fn paragraphs_with_speakers(&self) -> Vec<(&Paragraph, Option<&str>)> {
48        self.transcript_content
49            .transcript
50            .as_ref()
51            .map(|t| {
52                t.paragraphs
53                    .iter()
54                    .map(|p| (p, self.speaker_name(p.speaker)))
55                    .collect()
56            })
57            .unwrap_or_default()
58    }
59}
60
61/// Transcript content including speakers and full transcript.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct TranscriptContent {
64    /// Company ID (quartrId).
65    pub company_id: i64,
66    /// Event ID for this transcript.
67    pub event_id: i64,
68    /// Version of the transcript format.
69    #[serde(default)]
70    pub version: Option<String>,
71    /// Mapping of speaker IDs to speaker information.
72    #[serde(default)]
73    pub speaker_mapping: Vec<SpeakerMapping>,
74    /// The full transcript data.
75    #[serde(default)]
76    pub transcript: Option<TranscriptData>,
77}
78
79/// Mapping of a speaker ID to speaker information.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct SpeakerMapping {
82    /// Speaker ID (referenced in paragraphs).
83    pub speaker: i32,
84    /// Speaker details.
85    pub speaker_data: SpeakerData,
86}
87
88/// Information about a speaker.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct SpeakerData {
91    /// Company the speaker represents.
92    #[serde(default)]
93    pub company: Option<String>,
94    /// Speaker's name.
95    #[serde(default)]
96    pub name: String,
97    /// Speaker's role/title.
98    #[serde(default)]
99    pub role: Option<String>,
100}
101
102/// Full transcript data with paragraphs.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct TranscriptData {
105    /// Number of speakers in the call.
106    #[serde(default)]
107    pub number_of_speakers: i32,
108    /// Full text of the transcript.
109    #[serde(default)]
110    pub text: String,
111    /// Paragraphs (sections spoken by each speaker).
112    #[serde(default)]
113    pub paragraphs: Vec<Paragraph>,
114}
115
116/// A paragraph (section spoken by one speaker).
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Paragraph {
119    /// Speaker ID (use speaker_mapping to get name).
120    #[serde(default)]
121    pub speaker: i32,
122    /// Start time in seconds.
123    #[serde(default)]
124    pub start: f64,
125    /// End time in seconds.
126    #[serde(default)]
127    pub end: f64,
128    /// Full text of this paragraph.
129    #[serde(default)]
130    pub text: String,
131    /// Sentences in this paragraph.
132    #[serde(default)]
133    pub sentences: Vec<Sentence>,
134}
135
136/// A sentence within a paragraph.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Sentence {
139    /// Start time in seconds.
140    #[serde(default)]
141    pub start: f64,
142    /// End time in seconds.
143    #[serde(default)]
144    pub end: f64,
145    /// Text of the sentence.
146    #[serde(default)]
147    pub text: String,
148    /// Individual words with timing and confidence.
149    #[serde(default)]
150    pub words: Vec<Word>,
151}
152
153/// A word with timing and confidence information.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct Word {
156    /// The word (lowercase).
157    #[serde(default)]
158    pub word: String,
159    /// The word with punctuation.
160    #[serde(default)]
161    pub punctuated_word: String,
162    /// Start time in seconds.
163    #[serde(default)]
164    pub start: f64,
165    /// End time in seconds.
166    #[serde(default)]
167    pub end: f64,
168    /// Confidence score (0.0 - 1.0).
169    #[serde(default)]
170    pub confidence: f64,
171}
172
173/// Metadata about the transcript.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct TranscriptMetadata {
177    /// Date of the earnings call (Unix timestamp).
178    #[serde(default)]
179    pub date: i64,
180    /// Event ID.
181    #[serde(default)]
182    pub event_id: i64,
183    /// Type of event (e.g., "Earnings Call").
184    #[serde(default)]
185    pub event_type: String,
186    /// Fiscal period (e.g., "Q4").
187    #[serde(default)]
188    pub fiscal_period: String,
189    /// Fiscal year.
190    #[serde(default)]
191    pub fiscal_year: i32,
192    /// Whether this is the latest transcript.
193    #[serde(default)]
194    pub is_latest: bool,
195    /// S3 URL for the transcript data.
196    #[serde(default)]
197    pub s3_url: String,
198    /// Title (e.g., "Q4 2025").
199    #[serde(default)]
200    pub title: String,
201    /// Transcript ID.
202    #[serde(default)]
203    pub transcript_id: i64,
204    /// Transcript type (e.g., "IN_HOUSE").
205    #[serde(default, rename = "type")]
206    pub transcript_type: String,
207    /// Last updated timestamp.
208    #[serde(default)]
209    pub updated: i64,
210}
211
212/// Transcript with metadata from the earnings call list.
213///
214/// Used when fetching multiple transcripts.
215#[derive(Debug, Clone, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct TranscriptWithMeta {
218    /// Event ID.
219    pub event_id: String,
220    /// Fiscal quarter (e.g., "Q1", "Q2", "Q3", "Q4").
221    pub quarter: Option<String>,
222    /// Fiscal year.
223    pub year: Option<i32>,
224    /// Title of the earnings call.
225    pub title: String,
226    /// URL to the earnings call page.
227    pub url: String,
228    /// The full transcript.
229    pub transcript: Transcript,
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_deserialize_transcript() {
238        let json = r#"{
239            "transcriptContent": {
240                "company_id": 4742,
241                "event_id": 369370,
242                "version": "1.0.0",
243                "speaker_mapping": [
244                    {
245                        "speaker": 0,
246                        "speaker_data": {
247                            "company": "Apple",
248                            "name": "Tim Cook",
249                            "role": "CEO"
250                        }
251                    }
252                ],
253                "transcript": {
254                    "number_of_speakers": 15,
255                    "text": "Hello everyone...",
256                    "paragraphs": []
257                }
258            },
259            "transcriptMetadata": {
260                "date": 1761858000,
261                "eventId": 369370,
262                "eventType": "Earnings Call",
263                "fiscalPeriod": "Q4",
264                "fiscalYear": 2025,
265                "isLatest": true,
266                "title": "Q4 2025"
267            }
268        }"#;
269
270        let transcript: Transcript = serde_json::from_str(json).unwrap();
271        assert_eq!(transcript.transcript_content.company_id, 4742);
272        assert_eq!(transcript.quarter(), "Q4");
273        assert_eq!(transcript.year(), 2025);
274        assert_eq!(transcript.speaker_name(0), Some("Tim Cook"));
275    }
276}