From 0901c6462bf6dd9040484b5336efac6d3a86d719 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 16 Jul 2026 18:10:13 -0400 Subject: [PATCH 1/5] fix: restore deployable AI backend Restore missing API models and add cloud-init automation so a fresh DigitalOcean host can start the full stack without manual patching. Co-authored-by: Cursor --- scripts/do-cloud-init.yaml | 128 ++++++++++ services/ai-backend/app/models/__init__.py | 53 +++++ services/ai-backend/app/models/audio.py | 15 ++ services/ai-backend/app/models/command.py | 33 +++ services/ai-backend/app/models/engagement.py | 222 ++++++++++++++++++ services/ai-backend/app/models/generation.py | 36 +++ .../ai-backend/app/models/transcription.py | 34 +++ services/ai-backend/app/routes/engagement.py | 2 +- 8 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 scripts/do-cloud-init.yaml create mode 100644 services/ai-backend/app/models/__init__.py create mode 100644 services/ai-backend/app/models/audio.py create mode 100644 services/ai-backend/app/models/command.py create mode 100644 services/ai-backend/app/models/engagement.py create mode 100644 services/ai-backend/app/models/generation.py create mode 100644 services/ai-backend/app/models/transcription.py diff --git a/scripts/do-cloud-init.yaml b/scripts/do-cloud-init.yaml new file mode 100644 index 0000000000..56600061ba --- /dev/null +++ b/scripts/do-cloud-init.yaml @@ -0,0 +1,128 @@ +#cloud-config +package_update: true +package_upgrade: false + +packages: + - git + - curl + - ca-certificates + - gnupg + - jq + - ufw + - python3 + - openssl + +write_files: + - path: /opt/opencut/deploy.sh + permissions: "0755" + content: | + #!/usr/bin/env bash + set -euo pipefail + exec > >(tee -a /var/log/opencut-deploy.log) 2>&1 + + echo "[opencut] deploy started $(date -u +%Y-%m-%dT%H:%M:%SZ)" + + PUBLIC_IP="$(curl -fsS http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)" + AUTH_SECRET="$(openssl rand -hex 32)" + APP_DIR=/opt/opencut/OpenCut-AI + + if ! command -v docker >/dev/null 2>&1; then + curl -fsSL https://get.docker.com | sh + systemctl enable --now docker + fi + + mkdir -p /opt/opencut + if [ ! -d "$APP_DIR/.git" ]; then + git clone --depth 1 https://github.com/Ekaanth/OpenCut-AI.git "$APP_DIR" + fi + cd "$APP_DIR" + + # Bake public URLs into the Next.js build (NEXT_PUBLIC_* is compile-time) + sed -i \ + -e "s|ENV NEXT_PUBLIC_SITE_URL=\"http://localhost:3000\"|ENV NEXT_PUBLIC_SITE_URL=\"http://${PUBLIC_IP}:3100\"|" \ + -e "s|ENV NEXT_PUBLIC_AI_BACKEND_URL=\"http://localhost:8420\"|ENV NEXT_PUBLIC_AI_BACKEND_URL=\"http://${PUBLIC_IP}:8420\"|" \ + apps/web/Dockerfile + + cat > .env < tuple[str, str]: + """Map a 0–100 composite score to (grade letter, label).""" + if composite >= 85: + return "A", "Excellent" + if composite >= 70: + return "B", "Strong" + if composite >= 50: + return "C", "Average" + if composite >= 35: + return "D", "Below average" + return "F", "Needs work" + + +class EngagementScore(BaseModel): + """Full engagement breakdown with composite score and suggestions.""" + + hook: HookScore = Field(default_factory=HookScore) + curiosity: CuriosityScore = Field(default_factory=CuriosityScore) + energy: EnergyScore = Field(default_factory=EnergyScore) + audio_sync: AudioSyncScore = Field(default_factory=AudioSyncScore) + face_presence: FacePresenceScore = Field(default_factory=FacePresenceScore) + emotional_arc: EmotionalArcScore = Field(default_factory=EmotionalArcScore) + virality: ViralityScore = Field(default_factory=ViralityScore) + suggestions: list[EnhancementSuggestion] = Field(default_factory=list) + + def compute_composite(self) -> float: + """Weighted composite using configured engagement weights.""" + from app.config import settings + + return ( + self.hook.composite * settings.ENGAGEMENT_HOOK_WEIGHT + + self.curiosity.composite * settings.ENGAGEMENT_CURIOSITY_WEIGHT + + self.virality.composite * settings.ENGAGEMENT_VIRALITY_WEIGHT + + self.energy.composite * settings.ENGAGEMENT_ENERGY_WEIGHT + + self.emotional_arc.composite * settings.ENGAGEMENT_EMOTION_WEIGHT + + self.audio_sync.composite * settings.ENGAGEMENT_AUDIO_SYNC_WEIGHT + + self.face_presence.composite * settings.ENGAGEMENT_FACE_WEIGHT + ) + + def to_response(self) -> dict: + """Serialize to the API shape expected by the web client.""" + composite = round(min(100.0, max(0.0, self.compute_composite())), 1) + grade, grade_label = _grade_for_score(composite) + return { + "hook": self.hook.model_dump(), + "curiosity": self.curiosity.model_dump(), + "energy": self.energy.model_dump(), + "audio_sync": self.audio_sync.model_dump(), + "face_presence": self.face_presence.model_dump(), + "emotional_arc": self.emotional_arc.model_dump(), + "virality": self.virality.model_dump(), + "suggestions": [s.model_dump() for s in self.suggestions], + "composite": composite, + "grade": grade, + "grade_label": grade_label, + } + + @property + def composite(self) -> float: + return self.compute_composite() + + +# ── Request models ──────────────────────────────────────────────────── + + +class ScoreClipRequest(BaseModel): + """Score a single clip from transcript / audio / video paths.""" + + audio_path: str | None = None + video_path: str | None = None + transcript_text: str = "" + transcript_segments: list[dict] | None = None + start: float = 0.0 + end: float = 0.0 + title: str | None = None + + +class ScoreBatchRequest(BaseModel): + """Batch scoring request for multiple clips.""" + + clips: list[ScoreClipRequest] = Field(default_factory=list) + + +class ScoredClip(BaseModel): + """A detected clip with engagement score attached.""" + + index: int = 0 + title: str = "" + start: float = 0.0 + end: float = 0.0 + transcript_preview: str = "" + tags: list[str] = Field(default_factory=list) + engagement: EngagementScore = Field(default_factory=EngagementScore) + + @computed_field # type: ignore[prop-decorator] + @property + def duration(self) -> float: + return max(0.0, self.end - self.start) + + +# ── YouTube / jobs ──────────────────────────────────────────────────── + + +class YouTubeVideoMeta(BaseModel): + """Metadata for an ingested YouTube video.""" + + video_id: str + title: str = "Untitled" + channel_name: str = "Unknown" + channel_id: str = "" + duration_seconds: int = 0 + thumbnail_url: str = "" + upload_date: str = "" + view_count: int | None = None + is_live: bool = False + is_private: bool = False + warning: str | None = None + + +class JobStatus(BaseModel): + """Background job status for YouTube / clip pipelines.""" + + job_id: str + status: str = "pending" + progress: float = 0.0 + message: str = "" + result: dict | None = None + error: str | None = None diff --git a/services/ai-backend/app/models/generation.py b/services/ai-backend/app/models/generation.py new file mode 100644 index 0000000000..e852f31ddb --- /dev/null +++ b/services/ai-backend/app/models/generation.py @@ -0,0 +1,36 @@ +"""Image generation and infographic request models.""" + +from pydantic import BaseModel, ConfigDict, Field + + +class ImageGenParams(BaseModel): + """Parameters for text-to-image generation (proxied to image-service).""" + + model_config = ConfigDict(populate_by_name=True) + + prompt: str + negative_prompt: str = Field(default="", alias="negativePrompt") + width: int = 512 + height: int = 512 + steps: int = 20 + guidance_scale: float = Field(default=7.5, alias="guidanceScale") + seed: int | None = None + model: str | None = None + + +class EnhancePromptRequest(BaseModel): + """Request to expand a short prompt into a detailed diffusion prompt.""" + + prompt: str + style: str = "photorealistic" + + +class InfographicRequest(BaseModel): + """Request to render a simple infographic overlay PNG.""" + + topic: str + data_points: list[dict] = Field(default_factory=list) + style: str = "modern" + width: int = 1080 + height: int = 1080 + background_color: tuple[int, int, int, int] | str = (0, 0, 0, 0) diff --git a/services/ai-backend/app/models/transcription.py b/services/ai-backend/app/models/transcription.py new file mode 100644 index 0000000000..ff18d0e754 --- /dev/null +++ b/services/ai-backend/app/models/transcription.py @@ -0,0 +1,34 @@ +"""Whisper transcription result models.""" + +from pydantic import BaseModel, Field + + +class TranscriptionWord(BaseModel): + """Word-level timestamp from faster-whisper.""" + + word: str + start: float + end: float + probability: float = 0.0 + + +class TranscriptionSegment(BaseModel): + """A transcribed segment with optional word timings.""" + + id: int + text: str + start: float + end: float + words: list[TranscriptionWord] = Field(default_factory=list) + avg_logprob: float = 0.0 + no_speech_prob: float = 0.0 + speaker: str | None = None + + +class TranscriptionResult(BaseModel): + """Full transcription output.""" + + text: str = "" + segments: list[TranscriptionSegment] = Field(default_factory=list) + language: str = "" + duration: float = 0.0 diff --git a/services/ai-backend/app/routes/engagement.py b/services/ai-backend/app/routes/engagement.py index 5666bcda9d..22d0e79406 100644 --- a/services/ai-backend/app/routes/engagement.py +++ b/services/ai-backend/app/routes/engagement.py @@ -9,7 +9,7 @@ import uuid from fastapi import APIRouter, File, Form, HTTPException, UploadFile -from pydantic import BaseModel +from pydantic import BaseModel, Field from app.config import settings from app.models.engagement import ( From 75e4bda756e9c386e9871fa629d63ce20b5f37fe Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 16 Jul 2026 18:39:52 -0400 Subject: [PATCH 2/5] fix: stabilize TTS and web healthchecks for VPS deploy Pin torchaudio to match torch 2.5.1, stop TTS from autoloading on 8GB hosts, and use a Node-based web healthcheck since Alpine has no curl. Co-authored-by: Cursor --- docker-compose.yml | 6 +++--- services/tts-service/requirements.lock | 2 +- services/tts-service/requirements.txt | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5e548bce4f..b5384e62a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -135,11 +135,11 @@ services: volumes: - tts_models:/root/.cache environment: - - TTS_AUTOLOAD=true + - TTS_AUTOLOAD=false deploy: resources: limits: - memory: 6g + memory: 3g healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8422/health || exit 1"] interval: 30s @@ -293,7 +293,7 @@ services: ai-backend: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 30s timeout: 10s retries: 5 diff --git a/services/tts-service/requirements.lock b/services/tts-service/requirements.lock index 16957e10d9..4f883e1f8b 100644 --- a/services/tts-service/requirements.lock +++ b/services/tts-service/requirements.lock @@ -408,7 +408,7 @@ torch==2.5.1 # coqui-tts # coqui-tts-trainer # encodec -torchaudio==2.11.0 +torchaudio==2.5.1 # via # coqui-tts # encodec diff --git a/services/tts-service/requirements.txt b/services/tts-service/requirements.txt index d38e60f6c9..f52c470515 100644 --- a/services/tts-service/requirements.txt +++ b/services/tts-service/requirements.txt @@ -3,4 +3,5 @@ uvicorn[standard]==0.30.0 python-multipart==0.0.9 aiofiles==24.1.0 torch>=2.1.0,<2.6.0 +torchaudio==2.5.1 coqui-tts==0.24.2 From fa9628ad609752478c3426a1dc28778bf7736332 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 17 Jul 2026 00:57:43 -0400 Subject: [PATCH 3/5] fix: remove browser localhost/telemetry calls and harden UUID generation - Gate Databuddy analytics behind NEXT_PUBLIC_DATABUDDY_CLIENT_ID (was hardcoded, causing 402 batch/errors requests) - Route AI commit messages through the AI backend only; drop direct browser calls to localhost:11434 Ollama - Fix chat/stream payload shape (message field) and parse NDJSON response, fixing 422 errors - Make Ollama health URL configurable via NEXT_PUBLIC_OLLAMA_URL - Replace remaining crypto.randomUUID() uses with generateUUID() fallback for insecure contexts Co-authored-by: Cursor --- apps/web/src/app/layout.tsx | 26 ++++---- .../components/editor/ai/ai-panel-wrapper.tsx | 11 ++-- .../editor/panels/assets/views/ai-studio.tsx | 5 +- apps/web/src/hooks/use-service-health.ts | 2 +- .../src/services/version/ai-commit-message.ts | 59 ++++++------------- apps/web/src/stores/ai-store.ts | 3 +- apps/web/src/utils/id.ts | 18 +++++- 7 files changed, 60 insertions(+), 64 deletions(-) diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 981cb201c2..a0c8c3bf28 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -48,18 +48,20 @@ export default function RootLayout({ > -