1use axum::{
2 extract::{Path, Query, State},
3 http::StatusCode,
4 response::{sse::Event, IntoResponse, Sse},
5 Json,
6};
7use futures::stream::{self, Stream};
8use serde::{Deserialize, Serialize};
9use std::{convert::Infallible, time::Duration};
10use tracing::{error, info, warn};
11use validator::Validate;
12
13use super::{
14 jobs::{JobStatus, PdfJob},
15 requests::{LatexRequest, PdfRequest, StrokesRequest, TextRequest},
16 responses::{ErrorResponse, PdfResponse, TextResponse},
17 state::AppState,
18};
19
20pub async fn get_health() -> impl IntoResponse {
22 #[derive(Serialize)]
23 struct Health {
24 status: &'static str,
25 version: &'static str,
26 }
27
28 Json(Health {
29 status: "ok",
30 version: env!("CARGO_PKG_VERSION"),
31 })
32}
33
34pub async fn process_text(
41 State(_state): State<AppState>,
42 Json(request): Json<TextRequest>,
43) -> Result<Json<TextResponse>, ErrorResponse> {
44 info!("Processing text OCR request");
45
46 request.validate().map_err(|e| {
48 warn!("Invalid request: {:?}", e);
49 ErrorResponse::validation_error(format!("Validation failed: {}", e))
50 })?;
51
52 let image_data = match request.get_image_data().await {
54 Ok(data) => data,
55 Err(e) => {
56 error!("Failed to get image data: {:?}", e);
57 return Err(ErrorResponse::internal_error("Failed to process image"));
58 }
59 };
60
61 if image_data.is_empty() {
63 return Err(ErrorResponse::validation_error("Image data is empty"));
64 }
65
66 Err(ErrorResponse::service_unavailable(
69 "OCR service not fully configured. ONNX models are required for OCR processing. \
70 Please download compatible models (PaddleOCR, TrOCR) and configure the model directory. \
71 See documentation at /docs/MODEL_SETUP.md for setup instructions."
72 ))
73}
74
75pub async fn process_strokes(
80 State(_state): State<AppState>,
81 Json(request): Json<StrokesRequest>,
82) -> Result<Json<TextResponse>, ErrorResponse> {
83 info!("Processing strokes request with {} strokes", request.strokes.len());
84
85 request.validate().map_err(|e| {
86 ErrorResponse::validation_error(format!("Validation failed: {}", e))
87 })?;
88
89 if request.strokes.is_empty() {
91 return Err(ErrorResponse::validation_error("No strokes provided"));
92 }
93
94 Err(ErrorResponse::service_unavailable(
96 "Stroke recognition service not configured. ONNX models required for ink recognition."
97 ))
98}
99
100pub async fn process_latex(
105 State(_state): State<AppState>,
106 Json(request): Json<LatexRequest>,
107) -> Result<Json<TextResponse>, ErrorResponse> {
108 info!("Processing legacy LaTeX request");
109
110 request.validate().map_err(|e| {
111 ErrorResponse::validation_error(format!("Validation failed: {}", e))
112 })?;
113
114 Err(ErrorResponse::service_unavailable(
116 "LaTeX recognition service not configured. ONNX models required."
117 ))
118}
119
120pub async fn process_pdf(
122 State(state): State<AppState>,
123 Json(request): Json<PdfRequest>,
124) -> Result<Json<PdfResponse>, ErrorResponse> {
125 info!("Creating PDF processing job");
126
127 request.validate().map_err(|e| {
128 ErrorResponse::validation_error(format!("Validation failed: {}", e))
129 })?;
130
131 let job = PdfJob::new(request);
133 let job_id = job.id.clone();
134
135 state
137 .job_queue
138 .enqueue(job)
139 .await
140 .map_err(|e| {
141 error!("Failed to enqueue job: {:?}", e);
142 ErrorResponse::internal_error("Failed to create PDF job")
143 })?;
144
145 let response = PdfResponse {
146 pdf_id: job_id,
147 status: JobStatus::Processing,
148 message: Some("PDF processing started".to_string()),
149 result: None,
150 error: None,
151 };
152
153 Ok(Json(response))
154}
155
156pub async fn get_pdf_status(
158 State(state): State<AppState>,
159 Path(id): Path<String>,
160) -> Result<Json<PdfResponse>, ErrorResponse> {
161 info!("Getting PDF job status: {}", id);
162
163 let status = state
164 .job_queue
165 .get_status(&id)
166 .await
167 .ok_or_else(|| ErrorResponse::not_found("Job not found"))?;
168
169 let response = PdfResponse {
170 pdf_id: id.clone(),
171 status: status.clone(),
172 message: Some(format!("Job status: {:?}", status)),
173 result: state.job_queue.get_result(&id).await,
174 error: state.job_queue.get_error(&id).await,
175 };
176
177 Ok(Json(response))
178}
179
180pub async fn delete_pdf_job(
182 State(state): State<AppState>,
183 Path(id): Path<String>,
184) -> Result<StatusCode, ErrorResponse> {
185 info!("Deleting PDF job: {}", id);
186
187 state
188 .job_queue
189 .cancel(&id)
190 .await
191 .map_err(|_| ErrorResponse::not_found("Job not found"))?;
192
193 Ok(StatusCode::NO_CONTENT)
194}
195
196pub async fn stream_pdf_results(
198 State(_state): State<AppState>,
199 Path(_id): Path<String>,
200) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
201 info!("Streaming PDF results for job: {}", _id);
202
203 let stream = stream::unfold(0, move |page| {
204
205 async move {
206 if page > 10 {
207 return None;
209 }
210
211 tokio::time::sleep(Duration::from_millis(500)).await;
212
213 let event = Event::default()
214 .json_data(serde_json::json!({
215 "page": page,
216 "text": format!("Content from page {}", page),
217 "progress": (page as f32 / 10.0) * 100.0
218 }))
219 .ok()?;
220
221 Some((Ok(event), page + 1))
222 }
223 });
224
225 Sse::new(stream)
226}
227
228pub async fn convert_document(
233 State(_state): State<AppState>,
234 Json(_request): Json<serde_json::Value>,
235) -> Result<Json<serde_json::Value>, ErrorResponse> {
236 info!("Converting document");
237
238 Err(ErrorResponse::not_implemented(
240 "Document conversion is not yet implemented. This feature requires additional backend services."
241 ))
242}
243
244#[derive(Deserialize)]
246pub struct HistoryQuery {
247 #[serde(default)]
248 page: u32,
249 #[serde(default = "default_limit")]
250 limit: u32,
251}
252
253fn default_limit() -> u32 {
254 50
255}
256
257pub async fn get_ocr_results(
263 State(_state): State<AppState>,
264 Query(params): Query<HistoryQuery>,
265) -> Result<Json<serde_json::Value>, ErrorResponse> {
266 info!("Getting OCR results history: page={}, limit={}", params.page, params.limit);
267
268 Ok(Json(serde_json::json!({
270 "results": [],
271 "total": 0,
272 "page": params.page,
273 "limit": params.limit,
274 "notice": "History storage not configured. Results are not persisted."
275 })))
276}
277
278pub async fn get_ocr_usage(
284 State(_state): State<AppState>,
285) -> Result<Json<serde_json::Value>, ErrorResponse> {
286 info!("Getting OCR usage statistics");
287
288 Ok(Json(serde_json::json!({
290 "requests_today": 0,
291 "requests_month": 0,
292 "quota_limit": null,
293 "quota_remaining": null,
294 "notice": "Usage tracking not configured. Statistics are not recorded."
295 })))
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[tokio::test]
303 async fn test_health_check() {
304 let response = get_health().await.into_response();
305 assert_eq!(response.status(), StatusCode::OK);
306 }
307}