Add AI translation for comments and video descriptions

Comments get a Translate button that calls the active LLM provider in Admin → AI
settings. Each commenter shows a small country flag inferred from their
video_views + profile_visits history (falls back to users.nationality); that
inferred country drives the language hint passed to the LLM so Portuguese
"Linda, linda, linda" translates to "Beautiful" instead of pass-through.

Description boxes get a line-paired translation grid: a shared Translate button
flips #vdbDescSplit into a two-column CSS grid where every source line sits
next to its translation on the same row. Backend splits by newline and asks
the LLM for a JSON map keyed by index so counts always match.

LlmLyricsService now has translate() + translateLines() with echo-retry (if the
model returns the input unchanged while source != target, retry once with a
stronger directive) and keeps the Ollama model resident via keep_alive=1h.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-10-02 12:18:20 +03:00
1 parent 20c8fdcdf3
commit 2d33e39b9b
8 files changed
+749 -22

No files matched your search

+56 -1
View File
@@ -8,6 +8,7 @@ use App\Models\Video;
use App\Notifications\NewCommentLikeNotification;
use App\Notifications\NewCommentNotification;
use App\Notifications\NewReplyNotification;
use App\Services\LlmLyricsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -15,7 +16,7 @@ class CommentController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['index']);
$this->middleware('auth')->except(['index', 'translate']);
}
public function index(Video $video)
@@ -34,6 +35,29 @@ class CommentController extends Controller
$commenter = Auth::user();
// Idempotency guard: a client that retries (duplicate listeners after an
// SPA swap, fast double-click, a flaky connection) can POST the identical
// body twice. Treat an identical comment from the same user to the same
// video within 5 seconds as the same submission and return the first one.
$existing = $video->comments()
->where('user_id', $commenter->id)
->where('body', $request->body)
->where(function ($q) use ($request) {
$request->parent_id
? $q->where('parent_id', $request->parent_id)
: $q->whereNull('parent_id');
})
->where('created_at', '>=', now()->subSeconds(5))
->latest('id')
->first();
if ($existing) {
return response()->json([
'success' => true,
'comment' => $existing->load('user:id,name,avatar'),
]);
}
$comment = $video->comments()->create([
'user_id' => $commenter->id,
'body' => $request->body,
@@ -89,6 +113,37 @@ class CommentController extends Controller
return response()->json(['success' => true]);
}
public function translate(Request $request, Comment $comment, LlmLyricsService $llm)
{
if (! $llm->isEnabled()) {
return response()->json(['error' => 'Translation is not configured'], 503);
}
$target = (string) $request->input('target', 'en');
// Derive a source-language hint from the comment author's most-frequent
// access country (video_views + profile_visits, falling back to their
// nationality setting). Never trust the client for this — it's used
// to disambiguate scripts the LLM might otherwise misidentify.
$topCountries = \App\Models\User::topCountriesFor([$comment->user_id]);
$sourceCountry = $topCountries[$comment->user_id] ?? null;
$sourceLang = \App\Data\CountryLanguages::primary($sourceCountry);
$translated = $llm->translate((string) $comment->body, $target, $sourceLang);
if ($translated === '') {
return response()->json(['error' => 'Translation failed'], 502);
}
return response()->json([
'success' => true,
'translated' => $translated,
'target' => $target,
'source_hint' => $sourceLang,
'author_country'=> $sourceCountry,
]);
}
public function like(Comment $comment)
{
$userId = Auth::id();