Refactor audio handling and S3 integration in LMS service
- Removed company-specific template rules from template application logic. - Enhanced question generation queries to support both 'imported-mysql' and 'imported-material' sources. - Introduced S3 audio storage functionality, including client setup and audio key generation. - Updated audio response evaluation to store audio files in S3 or fallback to DB. - Added new API routes for asset ingestion and ZIP import in CMS service. - Implemented role-based access control for audio responses in LMS service. - Created a smoke test script for validating audio roles and permissions. - Updated frontend to support course selection in audio evaluations.
This commit is contained in:
@@ -733,12 +733,10 @@ pub async fn process_transcription(
|
||||
}
|
||||
|
||||
let url = lesson.content_url.ok_or(StatusCode::BAD_REQUEST)?;
|
||||
let filename = url.trim_start_matches("/assets/");
|
||||
let file_path = format!("uploads/{}", filename);
|
||||
|
||||
// 2. Read file to verify it exists
|
||||
if !tokio::fs::metadata(&file_path).await.is_ok() {
|
||||
tracing::error!("File not found: {}", file_path);
|
||||
// 2. Validate media is reachable (local /assets or absolute URL)
|
||||
if read_lesson_media_bytes(&url).await.is_err() {
|
||||
tracing::error!("Media not accessible for transcription: {}", url);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@@ -855,11 +853,9 @@ pub async fn run_transcription_task(pool: PgPool, lesson_id: Uuid) -> Result<(),
|
||||
.map_err(|e| format!("Lesson fetch failed: {}", e))?;
|
||||
|
||||
let url = lesson.content_url.ok_or("No content URL")?;
|
||||
let filename = url.trim_start_matches("/assets/");
|
||||
let file_path = format!("uploads/{}", filename);
|
||||
|
||||
// 2. Set status to processing ONLY if it's still queued (not cancelled/idle)
|
||||
tracing::info!("Starting transcription for lesson {} (file: {})", lesson_id, file_path);
|
||||
tracing::info!("Starting transcription for lesson {} (media: {})", lesson_id, url);
|
||||
let rows_affected = sqlx::query("UPDATE lessons SET transcription_status = 'processing' WHERE id = $1 AND transcription_status = 'queued'")
|
||||
.bind(lesson_id)
|
||||
.execute(&pool)
|
||||
@@ -873,10 +869,11 @@ pub async fn run_transcription_task(pool: PgPool, lesson_id: Uuid) -> Result<(),
|
||||
}
|
||||
|
||||
// 3. Read file
|
||||
let file_data = tokio::fs::read(&file_path)
|
||||
let filename_for_whisper = extract_filename_from_content_url(&url);
|
||||
let file_data = read_lesson_media_bytes(&url)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let err = format!("File read failed ({}): {}", file_path, e);
|
||||
let err = format!("File read failed ({}): {}", url, e);
|
||||
tracing::error!("{}", err);
|
||||
err
|
||||
})?;
|
||||
@@ -889,7 +886,7 @@ pub async fn run_transcription_task(pool: PgPool, lesson_id: Uuid) -> Result<(),
|
||||
|
||||
// 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()))
|
||||
.part("file", reqwest::multipart::Part::bytes(file_data).file_name(filename_for_whisper))
|
||||
.text("model", "whisper-1")
|
||||
.text("response_format", "json");
|
||||
|
||||
@@ -1094,6 +1091,40 @@ fn format_vtt_timestamp(seconds: f64) -> String {
|
||||
format!("{:02}:{:02}:{:02}.{:03}", hours, mins, secs, millis)
|
||||
}
|
||||
|
||||
fn extract_filename_from_content_url(url: &str) -> String {
|
||||
url.rsplit('/')
|
||||
.next()
|
||||
.filter(|v| !v.is_empty())
|
||||
.unwrap_or("media.bin")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn read_lesson_media_bytes(url: &str) -> Result<Vec<u8>, String> {
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
let response = reqwest::Client::new()
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP read failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("HTTP read returned status {}", response.status()));
|
||||
}
|
||||
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP bytes read failed: {}", e))?;
|
||||
return Ok(bytes.to_vec());
|
||||
}
|
||||
|
||||
let filename = url.trim_start_matches("/assets/");
|
||||
let file_path = format!("uploads/{}", filename);
|
||||
tokio::fs::read(&file_path)
|
||||
.await
|
||||
.map_err(|e| format!("Local read failed ({}): {}", file_path, e))
|
||||
}
|
||||
|
||||
pub async fn summarize_lesson(
|
||||
Org(org_ctx): Org,
|
||||
claims: common::auth::Claims,
|
||||
|
||||
Reference in New Issue
Block a user