feat: Introduce external API endpoints with API key authentication and re-enable Whisper transcription and Ollama summarization.
This commit is contained in:
+3
-3
@@ -18,6 +18,6 @@ AI_PROVIDER=local
|
|||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
|
|
||||||
# Local AI (Ollama & Whisper)
|
# Local AI (Ollama & Whisper)
|
||||||
LOCAL_WHISPER_URL=http://localhost:8000
|
LOCAL_WHISPER_URL=http://t-800:9000
|
||||||
LOCAL_OLLAMA_URL=http://localhost:11434
|
LOCAL_OLLAMA_URL=http://t-800:11434
|
||||||
LOCAL_LLM_MODEL=llama3
|
LOCAL_LLM_MODEL=llama3.2:3b
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ services:
|
|||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- ollama
|
|
||||||
|
|
||||||
experience:
|
experience:
|
||||||
build:
|
build:
|
||||||
@@ -45,21 +44,6 @@ services:
|
|||||||
env_file: .env
|
env_file: .env
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- ollama
|
|
||||||
|
|
||||||
ollama:
|
|
||||||
image: ollama/ollama:latest
|
|
||||||
ports:
|
|
||||||
- "11434:11434"
|
|
||||||
volumes:
|
|
||||||
- ollama_data:/root/.ollama
|
|
||||||
#deploy:
|
|
||||||
# resources:
|
|
||||||
# reservations:
|
|
||||||
# devices:
|
|
||||||
# - driver: nvidia
|
|
||||||
# count: 1
|
|
||||||
# capabilities: [ gpu ]
|
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
build:
|
build:
|
||||||
@@ -78,4 +62,3 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
uploads_data:
|
uploads_data:
|
||||||
ollama_data:
|
|
||||||
|
|||||||
+44
-71
@@ -73,20 +73,6 @@ if ! command -v sqlx &> /dev/null; then
|
|||||||
cargo install sqlx-cli --no-default-features --features postgres
|
cargo install sqlx-cli --no-default-features --features postgres
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Hardware Detection
|
|
||||||
echo ""
|
|
||||||
echo "🔍 Detecting hardware..."
|
|
||||||
HAS_NVIDIA=false
|
|
||||||
if command -v nvidia-smi &> /dev/null && nvidia-smi -L &> /dev/null; then
|
|
||||||
echo "🚀 NVIDIA GPU Detected!"
|
|
||||||
HAS_NVIDIA=true
|
|
||||||
elif command -v lspci &> /dev/null && lspci | grep -i nvidia &> /dev/null; then
|
|
||||||
echo "🚀 NVIDIA GPU Detected (lspci)!"
|
|
||||||
HAS_NVIDIA=true
|
|
||||||
else
|
|
||||||
echo "💻 No NVIDIA GPU found. Using CPU mode."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Environment Configuration
|
# 4. Environment Configuration
|
||||||
echo ""
|
echo ""
|
||||||
if [ ! -f ".env" ]; then
|
if [ ! -f ".env" ]; then
|
||||||
@@ -107,28 +93,22 @@ update_env() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto-configure AI variables based on hardware
|
# 5. Remote AI Configuration
|
||||||
if [ "$HAS_NVIDIA" = true ]; then
|
echo ""
|
||||||
update_env "LOCAL_LLM_MODEL" "llama3.2:1b"
|
echo "🔍 Configuring Remote AI Services..."
|
||||||
# Uncomment GPU deploy section in docker-compose.yml while preserving indentation
|
read -p "Enter Remote Ollama URL [http://t-800:11434]: " REMOTE_OLLAMA_URL
|
||||||
sed -i 's/^ #deploy:/ deploy:/' docker-compose.yml
|
REMOTE_OLLAMA_URL=${REMOTE_OLLAMA_URL:-http://t-800:11434}
|
||||||
sed -i 's/^ # resources:/ resources:/' docker-compose.yml
|
read -p "Enter Remote Whisper URL [http://t-800:9000]: " REMOTE_WHISPER_URL
|
||||||
sed -i 's/^ # reservations:/ reservations:/' docker-compose.yml
|
REMOTE_WHISPER_URL=${REMOTE_WHISPER_URL:-http://t-800:9000}
|
||||||
sed -i 's/^ # devices:/ devices:/' docker-compose.yml
|
read -p "Enter Model name (on remote server) [llama3.2:3b]: " LLM_MODEL
|
||||||
sed -i 's/^ # - driver: nvidia/ - driver: nvidia/' docker-compose.yml
|
LLM_MODEL=${LLM_MODEL:-llama3.2:3b}
|
||||||
sed -i 's/^ # count: 1/ count: 1/' docker-compose.yml
|
|
||||||
sed -i 's/^ # capabilities: \[ gpu \]/ capabilities: [ gpu ]/' docker-compose.yml
|
update_env "AI_PROVIDER" "local"
|
||||||
else
|
update_env "LOCAL_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
||||||
update_env "LOCAL_LLM_MODEL" "phi3:mini"
|
update_env "LOCAL_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
||||||
# Comment GPU deploy section in docker-compose.yml
|
update_env "LOCAL_LLM_MODEL" "$LLM_MODEL"
|
||||||
sed -i 's/^ deploy:/ #deploy:/' docker-compose.yml
|
|
||||||
sed -i 's/^ resources:/ # resources:/' docker-compose.yml
|
# AI setup is now purely remote. Skipping local container configuration.
|
||||||
sed -i 's/^ reservations:/ # reservations:/' docker-compose.yml
|
|
||||||
sed -i 's/^ devices:/ # devices:/' docker-compose.yml
|
|
||||||
sed -i 's/^ - driver: nvidia/ # - driver: nvidia/' docker-compose.yml
|
|
||||||
sed -i 's/^ count: 1/ # count: 1/' docker-compose.yml
|
|
||||||
sed -i 's/^ capabilities: \[ gpu \]/ # capabilities: [ gpu ]/' docker-compose.yml
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ask for DB credentials if not set
|
# Ask for DB credentials if not set
|
||||||
if ! grep -q "DATABASE_URL=" .env || [[ $(grep "DATABASE_URL=" .env | cut -d'=' -f2) == "" ]]; then
|
if ! grep -q "DATABASE_URL=" .env || [[ $(grep "DATABASE_URL=" .env | cut -d'=' -f2) == "" ]]; then
|
||||||
@@ -138,49 +118,36 @@ if ! grep -q "DATABASE_URL=" .env || [[ $(grep "DATABASE_URL=" .env | cut -d'='
|
|||||||
update_env "CMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_cms?sslmode=disable"
|
update_env "CMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_cms?sslmode=disable"
|
||||||
update_env "LMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_lms?sslmode=disable"
|
update_env "LMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_lms?sslmode=disable"
|
||||||
update_env "JWT_SECRET" "supersecretsecret"
|
update_env "JWT_SECRET" "supersecretsecret"
|
||||||
update_env "AI_PROVIDER" "local"
|
|
||||||
update_env "LOCAL_OLLAMA_URL" "http://ollama:11434"
|
|
||||||
update_env "NEXT_PUBLIC_CMS_API_URL" "http://localhost:3001"
|
update_env "NEXT_PUBLIC_CMS_API_URL" "http://localhost:3001"
|
||||||
update_env "NEXT_PUBLIC_LMS_API_URL" "http://localhost:3002"
|
update_env "NEXT_PUBLIC_LMS_API_URL" "http://localhost:3002"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. AI Stack Setup (Containerized)
|
# 5. AI Stack Setup (Skipped - using remote)
|
||||||
echo "⏳ Starting Ollama container..."
|
echo "🌐 Using remote AI services at $REMOTE_OLLAMA_URL and $REMOTE_WHISPER_URL"
|
||||||
docker compose up -d ollama
|
|
||||||
|
|
||||||
echo "⏳ Waiting for Ollama to be ready..."
|
|
||||||
until docker exec openccb-ollama-1 ollama list &> /dev/null; do sleep 2; done
|
|
||||||
|
|
||||||
echo "📥 Downloading models..."
|
|
||||||
if [ "$HAS_NVIDIA" = true ]; then
|
|
||||||
docker exec openccb-ollama-1 ollama pull llama3
|
|
||||||
else
|
|
||||||
docker exec openccb-ollama-1 ollama pull llama3:8b
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 6. Database Initialization (Integrated db-mgmt.sh)
|
# 6. Database Initialization (Integrated db-mgmt.sh)
|
||||||
echo ""
|
echo ""
|
||||||
read -p "Do you want a CLEAN installation? (This will DELETE all existing data) [y/N]: " CLEAN_INSTALL
|
read -p "Do you want a CLEAN installation? (This will DELETE all existing data) [y/N]: " CLEAN_INSTALL
|
||||||
if [[ "$CLEAN_INSTALL" =~ ^[Yy]$ ]]; then
|
if [[ "$CLEAN_INSTALL" =~ ^[Yy]$ ]]; then
|
||||||
echo "🐘 Resetting database for a clean installation..."
|
echo "🐘 Resetting database for a clean installation..."
|
||||||
docker compose down -v || true
|
sudo docker compose down -v || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🐘 Starting database with Docker..."
|
echo "🐘 Starting database with Docker..."
|
||||||
docker compose up -d db
|
sudo docker compose up -d db
|
||||||
|
|
||||||
echo "⏳ Waiting for database to be ready..."
|
echo "⏳ Waiting for database to be ready (container)..."
|
||||||
RETRIES=30
|
RETRIES=30
|
||||||
until docker exec openccb-db-1 pg_isready -U user &> /dev/null || [ $RETRIES -eq 0 ]; do
|
until sudo docker exec openccb-db-1 pg_isready -U user &> /dev/null || [ $RETRIES -eq 0 ]; do
|
||||||
echo -n "."
|
echo -n "."
|
||||||
sleep 1
|
sleep 1
|
||||||
RETRIES=$((RETRIES-1))
|
RETRIES=$((RETRIES-1))
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Reset retries for the second check and ensure we can actually execute queries
|
echo "⏳ Waiting for database port (host)..."
|
||||||
RETRIES=30
|
RETRIES=10
|
||||||
until docker exec openccb-db-1 psql -U user -d openccb -c "SELECT 1" &> /dev/null || [ $RETRIES -eq 0 ]; do
|
until curl -s localhost:5432 &> /dev/null || [ $RETRIES -eq 0 ]; do
|
||||||
echo -n "+"
|
echo -n "+"
|
||||||
sleep 1
|
sleep 1
|
||||||
RETRIES=$((RETRIES-1))
|
RETRIES=$((RETRIES-1))
|
||||||
@@ -188,10 +155,12 @@ done
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ $RETRIES -eq 0 ]; then
|
if [ $RETRIES -eq 0 ]; then
|
||||||
echo "❌ Database failed to start in time."
|
echo "⚠️ Wait for host port timed out, but continuing..."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Extra buffer for PostgreSQL initialization
|
||||||
|
sleep 2
|
||||||
|
|
||||||
CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2-)
|
CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2-)
|
||||||
LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2-)
|
LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2-)
|
||||||
|
|
||||||
@@ -204,7 +173,7 @@ DATABASE_URL=$LMS_URL sqlx migrate run --source services/lms-service/migrations
|
|||||||
# 7. System Initialization (Integrated init-system.sh)
|
# 7. System Initialization (Integrated init-system.sh)
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔍 Checking for existing administrator..."
|
echo "🔍 Checking for existing administrator..."
|
||||||
ADMIN_EXISTS=$(docker exec openccb-db-1 psql -U user -d openccb_cms -t -c "SELECT EXISTS (SELECT 1 FROM users WHERE role = 'admin');" | xargs 2>/dev/null || echo "f")
|
ADMIN_EXISTS=$(sudo docker exec openccb-db-1 psql -U user -d openccb_cms -t -c "SELECT EXISTS (SELECT 1 FROM users WHERE role = 'admin');" | xargs 2>/dev/null || echo "f")
|
||||||
|
|
||||||
if [ "$ADMIN_EXISTS" != "t" ]; then
|
if [ "$ADMIN_EXISTS" != "t" ]; then
|
||||||
echo "👤 Configure Initial Administrator"
|
echo "👤 Configure Initial Administrator"
|
||||||
@@ -220,26 +189,30 @@ fi
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "🚀 Starting all services..."
|
echo "🚀 Starting all services..."
|
||||||
docker compose up -d --build
|
sudo docker compose up -d --build
|
||||||
|
|
||||||
if [ "$ADMIN_EXISTS" != "t" ]; then
|
if [ "$ADMIN_EXISTS" != "t" ]; then
|
||||||
echo "⏳ Waiting for CMS API to be ready..."
|
echo "⏳ Waiting for CMS API to be ready..."
|
||||||
API_URL="http://localhost:3001"
|
API_URL="http://localhost:3001"
|
||||||
START_WAIT=$SECONDS
|
START_WAIT=$SECONDS
|
||||||
until curl -s "$API_URL/auth/login" &> /dev/null || [ $((SECONDS - START_WAIT)) -gt 60 ]; do sleep 2; done
|
PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
PAYLOAD=$(jq -n \
|
"email": "$ADMIN_EMAIL",
|
||||||
--arg email "$ADMIN_EMAIL" \
|
"password": "$ADMIN_PASS",
|
||||||
--arg password "$ADMIN_PASS" \
|
"full_name": "$ADMIN_NAME",
|
||||||
--arg full_name "$ADMIN_NAME" \
|
"organization_name": "$ORG_NAME",
|
||||||
--arg org_name "$ORG_NAME" \
|
"role": "admin"
|
||||||
--arg role "admin" \
|
}
|
||||||
'{email: $email, password: $password, full_name: $full_name, organization_name: $org_name, role: $role}')
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
RESPONSE=$(curl -s -X POST "$API_URL/auth/register" -H "Content-Type: application/json" -d "$PAYLOAD")
|
RESPONSE=$(curl -s -X POST "$API_URL/auth/register" -H "Content-Type: application/json" -d "$PAYLOAD")
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q "token"; then
|
if echo "$RESPONSE" | grep -q "token"; then
|
||||||
echo "✅ Success! Administrator created."
|
echo "✅ Success! Administrator created."
|
||||||
|
# Generate and show initial API Key
|
||||||
|
API_KEY=$(sudo docker exec openccb-db-1 psql -U user -d openccb_cms -t -c "SELECT api_key FROM organizations WHERE name = 'Default Organization' LIMIT 1;" | xargs)
|
||||||
|
echo "🔑 Initial API Key: $API_KEY"
|
||||||
else
|
else
|
||||||
echo "⚠️ Failed to create administrator."
|
echo "⚠️ Failed to create administrator."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add API key column to organizations table for external API authentication
|
||||||
|
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS api_key UUID DEFAULT gen_random_uuid();
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_organizations_api_key ON organizations(api_key);
|
||||||
@@ -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
|
.await
|
||||||
.map_err(|e| format!("File read failed ({}): {}", file_path, e))?;
|
.map_err(|e| format!("File read failed ({}): {}", file_path, e))?;
|
||||||
|
|
||||||
// 4. Transcription service is currently disabled in favor of Llama 3
|
// 4. Send to Whisper
|
||||||
tracing::warn!("Transcription service is disabled for lesson {}. Using Whisper removal policy.", lesson_id);
|
let whisper_url = env::var("LOCAL_WHISPER_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
sqlx::query(
|
// We assume a standard Whisper API (like faster-whisper-server or openai-compatible)
|
||||||
"UPDATE lessons SET transcription_status = 'failed' WHERE id = $1",
|
let form = reqwest::multipart::Form::new()
|
||||||
)
|
.part("file", reqwest::multipart::Part::bytes(file_data).file_name(filename.to_string()))
|
||||||
.bind(lesson_id)
|
.text("model", "whisper-1")
|
||||||
.execute(&pool)
|
.text("response_format", "json");
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to update status to failed: {}", e))?;
|
|
||||||
|
|
||||||
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(
|
pub async fn get_lesson_vtt(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod exporter;
|
|||||||
mod handlers;
|
mod handlers;
|
||||||
mod handlers_branding;
|
mod handlers_branding;
|
||||||
mod webhooks;
|
mod webhooks;
|
||||||
|
mod external_handlers;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
@@ -172,8 +173,14 @@ async fn main() {
|
|||||||
common::middleware::org_extractor_middleware,
|
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
|
// Rutas públicas que no requieren autenticación
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
|
.nest("/api/external", api_routes)
|
||||||
.route("/auth/register", post(handlers::register))
|
.route("/auth/register", post(handlers::register))
|
||||||
.route("/auth/login", post(handlers::login))
|
.route("/auth/login", post(handlers::login))
|
||||||
.route("/auth/sso/login/{org_id}", get(handlers::sso_login_init))
|
.route("/auth/sso/login/{org_id}", get(handlers::sso_login_init))
|
||||||
|
|||||||
Reference in New Issue
Block a user