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:
1 parent
20c8fdcdf3
commit
2d33e39b9b
8 files changed
+749
-22
No files matched your search
@@ -7,6 +7,8 @@ use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
@@ -56,8 +58,57 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'password' => 'hashed',
|
||||
'two_factor_enabled' => 'boolean',
|
||||
'notification_preferences' => 'array',
|
||||
// TAKEONE account link. The token is a live credential for that
|
||||
// platform, so it is encrypted at rest and never sent back to a browser.
|
||||
'takeone_token' => 'encrypted',
|
||||
'takeone_connected_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Can this user run TAKEONE lookups?
|
||||
*
|
||||
* A super admin always can: the platform holds a TAKEONE super-admin token
|
||||
* on their behalf, so they are connected with full reach without ever
|
||||
* pasting anything. Everyone else links their own account, and sees only
|
||||
* what that account may see.
|
||||
*/
|
||||
public function takeoneLinked(): bool
|
||||
{
|
||||
return $this->takeoneToken() !== null;
|
||||
}
|
||||
|
||||
/** True when the connection is the platform's, not one this user pasted. */
|
||||
public function takeoneManagedLink(): bool
|
||||
{
|
||||
$own = $this->takeone_token;
|
||||
|
||||
return ($own === null || $own === '')
|
||||
&& $this->isSuperAdmin()
|
||||
&& ! empty(config('services.takeone.admin_token'));
|
||||
}
|
||||
|
||||
/**
|
||||
* The token to talk to TAKEONE with. A personally linked account always
|
||||
* wins, so a super admin who deliberately links their own identity is not
|
||||
* silently upgraded back to the platform credential.
|
||||
*/
|
||||
public function takeoneToken(): ?string
|
||||
{
|
||||
$own = $this->takeone_token;
|
||||
|
||||
if ($own !== null && $own !== '') {
|
||||
return $own;
|
||||
}
|
||||
|
||||
if ($this->isSuperAdmin()) {
|
||||
$platform = config('services.takeone.admin_token');
|
||||
|
||||
return ! empty($platform) ? $platform : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Defaults for each preference key (true = on, false = off)
|
||||
private const NOTIF_DEFAULTS = [
|
||||
'notif_new_comment' => true,
|
||||
@@ -105,6 +156,55 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the most-frequent ISO2 country for each user id, aggregating
|
||||
* video_views + profile_visits and falling back to users.nationality.
|
||||
* Returns [user_id => 'BH', …]; keys without data are omitted.
|
||||
*/
|
||||
public static function topCountriesFor(array $userIds): array
|
||||
{
|
||||
$userIds = array_values(array_unique(array_filter($userIds, 'is_numeric')));
|
||||
if (! $userIds) return [];
|
||||
|
||||
$tally = [];
|
||||
$rows = DB::table('video_views')
|
||||
->selectRaw('user_id, country, COUNT(*) as c')
|
||||
->whereIn('user_id', $userIds)
|
||||
->whereNotNull('country')->where('country', '!=', '')
|
||||
->groupBy('user_id', 'country')->get();
|
||||
foreach ($rows as $r) {
|
||||
$cc = strtoupper($r->country);
|
||||
$tally[(int) $r->user_id][$cc] = ($tally[(int) $r->user_id][$cc] ?? 0) + (int) $r->c;
|
||||
}
|
||||
if (Schema::hasTable('profile_visits')) {
|
||||
$rows = DB::table('profile_visits')
|
||||
->selectRaw('visitor_user_id as user_id, country, COUNT(*) as c')
|
||||
->whereIn('visitor_user_id', $userIds)
|
||||
->whereNotNull('country')->where('country', '!=', '')
|
||||
->groupBy('visitor_user_id', 'country')->get();
|
||||
foreach ($rows as $r) {
|
||||
$cc = strtoupper($r->country);
|
||||
$tally[(int) $r->user_id][$cc] = ($tally[(int) $r->user_id][$cc] ?? 0) + (int) $r->c;
|
||||
}
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($tally as $uid => $counts) {
|
||||
arsort($counts);
|
||||
$out[$uid] = array_key_first($counts);
|
||||
}
|
||||
|
||||
// Fallback to users.nationality for anyone still unresolved.
|
||||
$missing = array_values(array_diff($userIds, array_keys($out)));
|
||||
if ($missing) {
|
||||
$nats = static::whereIn('id', $missing)
|
||||
->whereNotNull('nationality')->where('nationality', '!=', '')
|
||||
->pluck('nationality', 'id');
|
||||
foreach ($nats as $uid => $cc) $out[(int) $uid] = strtoupper($cc);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function generateUniqueUsername(string $name): string
|
||||
{
|
||||
$base = substr(Str::slug(trim($name)), 0, 18);
|
||||
|
||||
Reference in new issue
Block a user