feat: Introduce external API endpoints with API key authentication and re-enable Whisper transcription and Ollama summarization.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::{StatusCode, HeaderMap},
|
||||
};
|
||||
use common::models::Course;
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
async fn validate_api_key(headers: &HeaderMap, pool: &PgPool) -> Result<Uuid, StatusCode> {
|
||||
let api_key = headers
|
||||
.get("X-API-Key")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let org_id: Uuid = sqlx::query_scalar("SELECT id FROM organizations WHERE api_key = $1")
|
||||
.bind(Uuid::parse_str(api_key).map_err(|_| StatusCode::UNAUTHORIZED)?)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
Ok(org_id)
|
||||
}
|
||||
|
||||
pub async fn create_course_external(
|
||||
State(pool): State<PgPool>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> Result<Json<Course>, StatusCode> {
|
||||
let org_id = validate_api_key(&headers, &pool).await?;
|
||||
|
||||
// We reuse the internal logic but with the org_id from the API key
|
||||
// We need to provide a mock claims for handlers::create_course or refactor it.
|
||||
// Simplifying for now: direct DB call or calling handlers with constructed context.
|
||||
|
||||
let title = payload.get("title").and_then(|t| t.as_str()).ok_or(StatusCode::BAD_REQUEST)?;
|
||||
let description = payload.get("description").and_then(|d| d.as_str());
|
||||
|
||||
let course = sqlx::query_as::<_, Course>(
|
||||
"INSERT INTO courses (organization_id, title, description, instructor_id, pacing_mode)
|
||||
VALUES ($1, $2, $3, '00000000-0000-0000-0000-000000000001', $4) RETURNING *"
|
||||
)
|
||||
.bind(org_id)
|
||||
.bind(title)
|
||||
.bind(description)
|
||||
.bind(payload.get("pacing_mode").and_then(|p| p.as_str()).unwrap_or("self_paced"))
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("External course creation failed: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(course))
|
||||
}
|
||||
|
||||
pub async fn get_course_external(
|
||||
State(pool): State<PgPool>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let org_id = validate_api_key(&headers, &pool).await?;
|
||||
|
||||
let course = sqlx::query_as::<_, Course>("SELECT * FROM courses WHERE id = $1 AND organization_id = $2")
|
||||
.bind(id)
|
||||
.bind(org_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
Ok(Json(json!({ "course": course })))
|
||||
}
|
||||
|
||||
pub async fn trigger_transcription_external(
|
||||
State(pool): State<PgPool>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let org_id = validate_api_key(&headers, &pool).await?;
|
||||
|
||||
// Verify lesson belongs to org
|
||||
let _ = sqlx::query("SELECT 1 FROM lessons WHERE id = $1 AND organization_id = $2")
|
||||
.bind(id)
|
||||
.bind(org_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
// Queue transcription
|
||||
sqlx::query("UPDATE lessons SET transcription_status = 'queued' WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
}
|
||||
@@ -730,18 +730,91 @@ pub async fn run_transcription_task(pool: PgPool, lesson_id: Uuid) -> Result<(),
|
||||
.await
|
||||
.map_err(|e| format!("File read failed ({}): {}", file_path, e))?;
|
||||
|
||||
// 4. Transcription service is currently disabled in favor of Llama 3
|
||||
tracing::warn!("Transcription service is disabled for lesson {}. Using Whisper removal policy.", lesson_id);
|
||||
// 4. Send to Whisper
|
||||
let whisper_url = env::var("LOCAL_WHISPER_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE lessons SET transcription_status = 'failed' WHERE id = $1",
|
||||
)
|
||||
.bind(lesson_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update status to failed: {}", e))?;
|
||||
// We assume a standard Whisper API (like faster-whisper-server or openai-compatible)
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.part("file", reqwest::multipart::Part::bytes(file_data).file_name(filename.to_string()))
|
||||
.text("model", "whisper-1")
|
||||
.text("response_format", "json");
|
||||
|
||||
return Err("Transcription service is currently disabled. All AI features now use Llama 3 directly.".to_string());
|
||||
let response = client.post(format!("{}/v1/audio/transcriptions", whisper_url))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Whisper request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let err_body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Whisper API error: {} - {}", status, err_body));
|
||||
}
|
||||
|
||||
let transcription_result: serde_json::Value = response.json().await
|
||||
.map_err(|e| format!("Failed to parse Whisper response: {}", e))?;
|
||||
|
||||
// 5. Update lesson with transcription
|
||||
sqlx::query("UPDATE lessons SET transcription = $1, transcription_status = 'completed' WHERE id = $2")
|
||||
.bind(&transcription_result)
|
||||
.bind(lesson_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update lesson with transcription: {}", e))?;
|
||||
|
||||
// 6. Optional: Trigger Summarization using Ollama
|
||||
let full_text = transcription_result["text"].as_str().unwrap_or("");
|
||||
if !full_text.is_empty() {
|
||||
if let Ok(summary) = generate_summary_with_ollama(full_text).await {
|
||||
let _ = sqlx::query("UPDATE lessons SET summary = $1 WHERE id = $2")
|
||||
.bind(summary)
|
||||
.bind(lesson_id)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_summary_with_ollama(text: &str) -> Result<String, String> {
|
||||
let base_url = env::var("LOCAL_OLLAMA_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
|
||||
let model = env::var("LOCAL_LLM_MODEL").unwrap_or_else(|_| "llama3.2:3b".to_string());
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let prompt = format!(
|
||||
"Resume el siguiente texto de forma concisa y estructurada en español:\n\n{}",
|
||||
text
|
||||
);
|
||||
|
||||
let response = client.post(format!("{}/v1/chat/completions", base_url))
|
||||
.json(&serde_json::json!({
|
||||
"model": model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
],
|
||||
"temperature": 0.5
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama summary request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err("Ollama summary API error".into());
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
|
||||
let summary = result["choices"][0]["message"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
pub async fn get_lesson_vtt(
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod exporter;
|
||||
mod handlers;
|
||||
mod handlers_branding;
|
||||
mod webhooks;
|
||||
mod external_handlers;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
@@ -172,8 +173,14 @@ async fn main() {
|
||||
common::middleware::org_extractor_middleware,
|
||||
));
|
||||
|
||||
let api_routes = Router::new()
|
||||
.route("/v1/courses", post(external_handlers::create_course_external))
|
||||
.route("/v1/courses/{id}", get(external_handlers::get_course_external))
|
||||
.route("/v1/lessons/{id}/transcribe", post(external_handlers::trigger_transcription_external));
|
||||
|
||||
// Rutas públicas que no requieren autenticación
|
||||
let public_routes = Router::new()
|
||||
.nest("/api/external", api_routes)
|
||||
.route("/auth/register", post(handlers::register))
|
||||
.route("/auth/login", post(handlers::login))
|
||||
.route("/auth/sso/login/{org_id}", get(handlers::sso_login_init))
|
||||
|
||||
Reference in New Issue
Block a user