Skip to main content

cooklang_import/
lib.rs

1pub mod builder;
2pub mod config;
3pub mod converters;
4pub mod error;
5pub mod images_to_text;
6pub(crate) mod model;
7pub mod pipelines;
8pub mod url_to_text;
9
10#[cfg(feature = "uniffi")]
11pub mod uniffi_bindings;
12
13// Re-export UniFFI types when feature is enabled
14#[cfg(feature = "uniffi")]
15pub use uniffi_bindings::*;
16
17// Public API re-exports
18pub use config::AiConfig;
19pub use converters::{ConversionMetadata, ConversionResult, TokenUsage};
20pub use error::ImportError;
21pub use images_to_text::ImageSource;
22pub use pipelines::RecipeComponents;
23
24// Advanced builder API (for users who need more control)
25pub use builder::{ImportResult, LlmProvider, RecipeImporter, RecipeImporterBuilder};
26
27/// Extract recipe components from a URL.
28///
29/// Returns `RecipeComponents` with text, metadata, and name fields.
30/// Empty strings are used for fields that couldn't be extracted.
31///
32/// # Arguments
33/// * `url` - The URL of the recipe webpage to fetch
34///
35/// # Returns
36/// * `Ok(RecipeComponents)` - The extracted recipe components
37/// * `Err(ImportError)` - If the URL cannot be fetched or parsed
38///
39/// # Example
40/// ```no_run
41/// use cooklang_import::url_to_recipe;
42///
43/// #[tokio::main]
44/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
45///     let recipe = url_to_recipe("https://example.com/recipe").await?;
46///     println!("Name: {}", recipe.name);
47///     println!("Text: {}", recipe.text);
48///     Ok(())
49/// }
50/// ```
51pub async fn url_to_recipe(url: &str) -> Result<RecipeComponents, ImportError> {
52    pipelines::url::process(url)
53        .await
54        .map_err(|e| ImportError::ExtractionError(e.to_string()))
55}
56
57/// Extract recipe components from images.
58///
59/// Returns `RecipeComponents` with text extracted via OCR.
60/// Metadata contains the source info, name is typically empty.
61///
62/// Requires GOOGLE_API_KEY environment variable to be set.
63///
64/// # Arguments
65/// * `images` - Vector of image sources (paths or base64-encoded data)
66///
67/// # Returns
68/// * `Ok(RecipeComponents)` - The extracted recipe components
69/// * `Err(ImportError)` - If OCR fails
70///
71/// # Example
72/// ```no_run
73/// use cooklang_import::{image_to_recipe, ImageSource};
74///
75/// #[tokio::main]
76/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
77///     let images = vec![ImageSource::Path("/path/to/recipe.jpg".to_string())];
78///     let recipe = image_to_recipe(&images).await?;
79///     println!("Text: {}", recipe.text);
80///     Ok(())
81/// }
82/// ```
83pub async fn image_to_recipe(images: &[ImageSource]) -> Result<RecipeComponents, ImportError> {
84    pipelines::image::process(images)
85        .await
86        .map_err(|e| ImportError::ExtractionError(e.to_string()))
87}
88
89/// Parse text into recipe components.
90///
91/// If `extract` is true, uses LLM to extract structured recipe data.
92/// If `extract` is false, parses the text assuming it's already formatted
93/// with optional YAML frontmatter.
94///
95/// # Arguments
96/// * `text` - The recipe text
97/// * `extract` - Whether to use LLM extraction
98///
99/// # Returns
100/// * `Ok(RecipeComponents)` - The parsed recipe components
101/// * `Err(ImportError)` - If parsing or extraction fails
102///
103/// # Example
104/// ```no_run
105/// use cooklang_import::text_to_recipe;
106///
107/// #[tokio::main]
108/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
109///     let text = "2 eggs\n1 cup flour\n\nMix and bake at 350F.";
110///     let recipe = text_to_recipe(text, false).await?;
111///     println!("Text: {}", recipe.text);
112///     Ok(())
113/// }
114/// ```
115pub async fn text_to_recipe(text: &str, extract: bool) -> Result<RecipeComponents, ImportError> {
116    pipelines::text::process(text, extract)
117        .await
118        .map_err(|e| ImportError::ExtractionError(e.to_string()))
119}
120
121/// Convert recipe text to Cooklang format.
122///
123/// Takes recipe text (ingredients + instructions) and converts it
124/// to Cooklang format using an LLM. Returns the Cooklang text
125/// with optional YAML frontmatter if metadata/name are provided.
126///
127/// # Arguments
128/// * `components` - The recipe components to convert
129///
130/// # Returns
131/// * `Ok(String)` - The recipe in Cooklang format
132/// * `Err(ImportError)` - If conversion fails
133///
134/// # Example
135/// ```no_run
136/// use cooklang_import::{text_to_cooklang, RecipeComponents};
137///
138/// #[tokio::main]
139/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
140///     let components = RecipeComponents {
141///         text: "2 eggs\n1 cup flour\n\nMix and bake at 350F.".to_string(),
142///         metadata: String::new(),
143///         name: "Simple Cake".to_string(),
144///     };
145///     let cooklang = text_to_cooklang(&components).await?;
146///     println!("{}", cooklang);
147///     Ok(())
148/// }
149/// ```
150pub async fn text_to_cooklang(components: &RecipeComponents) -> Result<String, ImportError> {
151    match RecipeImporter::builder()
152        .text(&components.text)
153        .build()
154        .await?
155    {
156        ImportResult::Cooklang { mut content, .. } => {
157            // Prepend frontmatter if we have name or metadata
158            let has_name = !components.name.is_empty();
159            let has_metadata = !components.metadata.is_empty();
160
161            if has_name || has_metadata {
162                let mut frontmatter = String::from("---\n");
163                if has_name {
164                    let title_yaml = crate::pipelines::metadata_to_yaml(&[(
165                        "title".to_string(),
166                        components.name.clone(),
167                    )]);
168                    frontmatter.push_str(&title_yaml);
169                }
170                if has_metadata {
171                    frontmatter.push_str(&components.metadata);
172                    if !components.metadata.ends_with('\n') {
173                        frontmatter.push('\n');
174                    }
175                }
176                frontmatter.push_str("---\n\n");
177                content = frontmatter + &content;
178            }
179            Ok(content)
180        }
181        ImportResult::Components(_) => unreachable!("Default mode is Cooklang"),
182    }
183}