mod claude_code;
mod gemini_cli;
pub(crate) use claude_code::ClaudeCodeMatcher;
pub(crate) use gemini_cli::GeminiCliMatcher;
use crate::metadata_retrieval::{Episode, TVSeries};
use crate::speech_to_text::Transcript;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum EpisodeMatchingError {
#[error("AI service error: {0}")]
ServiceError(String),
#[error("Failed to parse AI response: {reason}\n\nFull LLM response:\n{response}")]
ParseError { reason: String, response: String },
#[error("No matching episode found in the series\n\nFull LLM response:\n{response}")]
NoMatchFound { response: String },
}
pub(crate) trait EpisodeMatcher {
fn match_episode(
&self,
transcript: &Transcript,
series: &TVSeries,
) -> Result<Episode, EpisodeMatchingError>;
}
pub(crate) trait SinglePromptGenerator {
fn generate_single_prompt(&self, transcript: &Transcript, series: &TVSeries) -> String;
}
pub(crate) struct NaivePromptGenerator;
impl Default for NaivePromptGenerator {
fn default() -> Self {
Self
}
}
impl SinglePromptGenerator for NaivePromptGenerator {
fn generate_single_prompt(&self, transcript: &Transcript, series: &TVSeries) -> String {
let mut prompt = String::new();
prompt.push_str("IMPORTANT: Your output to the following MUST be JSON in the FORMAT ");
prompt.push_str(r#"{"season": XX, "episode": YY}. "#);
prompt
.push_str("NOTHING ELSE IS TO BE RETURNED. ONLY EVER ANSWER WITH THIS JSON Structure.");
prompt.push_str("The JSON is to be encapsulated in a markdown jsonblock ```json\n\n");
prompt.push_str("Using this structure answer the following question:\n");
prompt.push_str("Based on the given Transcript of a tv series episode as well as a List of possible episode candidates ");
prompt.push_str(
"identified by their Season number, Episode number, title and short summary, ",
);
prompt.push_str("match the transcript to the best fitting short summary, to identify which episode the given transcript belongs to.\n\n");
prompt.push_str("Ultrathink about this and reflect on your reasoning, before providing ONLY THE REQUESTED ANSWER FORMAT.\n\n");
prompt.push_str("Here follows the mentioned data:\n\n");
prompt.push_str("=== TRANSCRIPT ===\n");
prompt.push_str(&format!("Language: {}\n\n", transcript.language));
prompt.push_str(&transcript.text);
prompt.push_str("\n\n");
prompt.push_str(&format!(
"=== EPISODE CANDIDATES FOR '{}' ===\n\n",
series.name
));
for season in &series.seasons {
prompt.push_str(&format!("--- SEASON {} ---\n", season.season_number));
for episode in &season.episodes {
prompt.push_str(&format!(
"Season: {}, Episode: {} - {}\n",
episode.season_number, episode.episode_number, episode.name
));
prompt.push_str(&format!("Summary: {}\n\n", episode.summary));
}
}
prompt
}
}