Ürünereklam
TerminéCollez le lien d'un produit et obtenez des publicités vidéo et image prêtes à diffuser, en turc et en anglais, chacune notée par un modèle de prédiction avant toute dépense. Le projet est né au BTK Akademi Hackathon en 2026.
- Mon rôle
- Design et développement, en solo
- Année
- 2026
- Développé avec
- Next.js · Inngest · Supabase · Gemini · Veo · ElevenLabs · Remotion · PyTorch on Modal · Polar
Faites défiler horizontalement pour voir tout le schéma.
Le problème
Des publicités en deux langues, au volume dont les boutiques en ligne ont besoin, passent en général par une agence au forfait. Même dans ce cas, personne ne sait quelle version marchera avant que le budget soit dépensé.
Comment c'est construit
Une application Next.js orchestre des jobs durables sur Inngest. Un scraper Python lit la page produit, avec un chemin de fetch durci pour les boutiques difficiles. Gemini écrit la stratégie, les textes et les frames, Veo ou Seedance fait le rendu de la vidéo, ElevenLabs fournit la voix, et Remotion ajoute des sous-titres calés mot par mot, le filigrane et l'appel à l'action. Un studio séparé produit des publicités en image statique et des textes publicitaires. Chaque variante passe ensuite par un modèle auto-hébergé qui prédit comment les gens vont y réagir (TRIBE v2), sur des GPU Modal, lancé puis interrogé par polling, car un rendu dépasse le timeout d'une Edge Function. Supabase garde l'état derrière la row-level security, avec des URL de médias signées à courte durée de vie. Les crédits sont tenus dans un registre append-only avec des débits et des remboursements atomiques, et le paiement passe par Polar, avec des prix en livres turques et en dollars.
Les décisions qui ont compté
Une étape en échec rembourse elle-même ses crédits
Le pipeline est composé de jobs durables, avec un handler d'échec pour chaque étape. Une étape qui plante rembourse ses crédits exactement une fois et libère le run au lieu de le laisser bloqué.
Les médias sont privés par défaut
Les rendus bruts sont dans un bucket privé, le SELECT au niveau des colonnes est révoqué et les liens vers les médias sont des URL signées à courte durée de vie. Aucune adresse de média ne peut être devinée, offre gratuite comprise.
Chaque publicité est notée avant que quelqu'un la paie
Aucune publicité ne sort du studio sans note. Le modèle classe les variantes avant que quelqu'un ne mette du budget sur l'une d'elles.
Code tiré du dépôt
async function safeRefund(
opts: Parameters<typeof refundCredits>[0],
): Promise<void> {
try {
await refundCredits(opts);
} catch (err) {
console.error(
`[billing][RECONCILE-NEEDED] refund failed user=${opts.userId} ` +1
`amount=${opts.amount} ref=${opts.refType}:${opts.refId} ` +
`key=${opts.idempotencyKey ?? "-"}`,
err,
);
}
}
// …
await safeRefund({
supa,
userId: campaign.user_id,
amount: campaignCreditCost({
videoCount: brief.videoCount ?? 1,
videoModel: brief.videoModel ?? "seedance",
durationSec: brief.durationSec,
}),
refType: "campaign",
refId: campaignId,
idempotencyKey: `refund:campaign-all:${campaignId}`,2
description: "Kampanya başlatılamadı — otomatik kredi iadesi",3
});
Ligne 8. Un remboursement qui échoue écrit un marqueur RECONCILE-NEEDED dans les logs au lieu de lever une exception, donc aucun crédit n'est perdu en silence et le pipeline libère quand même le run.
Ligne 27. Une clé d'idempotence par événement : le handler d'échec peut s'exécuter à nouveau, mais le registre n'enregistre le remboursement qu'une seule fois.
Ligne 28. Le motif du remboursement est en turc, parce que le produit est bilingue de bout en bout.
Où en est le projet
D'une démo de hackathon, c'est devenu un produit bilingue avec un scoring sur GPU. Il est désormais closed source, et le lancement payant est en préparation.