# Sous-titrer mes vidéos avec Whisper sur une carte graphique louée à la seconde

> Lancer Whisper en local faisait planter ma machine, alors je loue une RTX 4090 à la seconde chez Runpod. Six tâches mesurées le 3 novembre 2026 : attente, calcul, qualité du texte et facture. Le défaut trouvé en route n'était pas sur le GPU : mon script de sous-titres jetait 15 % des mots.


*Ou : Comment louer une carte graphique pour cinq secondes, et découvrir que le problème était dans les quarante lignes autour*

Mes vidéos sont montées avec Remotion, et chaque voix off passe par une transcription : elle fournit les sous-titres et le minutage de chaque mot, sur lequel se calent les animations. Whisper, le modèle de reconnaissance vocale d'OpenAI, fait ce travail très bien, à condition d'avoir le matériel qui suit. Chez moi, lancer Whisper en local faisait planter la machine. Depuis, la transcription part chez Runpod, sur un GPU loué à la seconde en mode serverless, avec le worker faster-whisper officiel. Le 3 novembre 2026, j'ai mesuré ce que ça donne vraiment : six tâches sur un extrait de 99 secondes, les modèles turbo et large-v3, à froid et à chaud. Le plus précis des deux transcrit l'extrait en 5,5 secondes, avec un seul mot de travers, pour une fraction de centime.

En préparant ces mesures, j'ai trouvé le vrai défaut de la chaîne, et il n'était pas sur le GPU. Whisper avait entendu « le FBI ». Mon script, lui, l'avait retiré du fichier de sous-titres. En tout, 165 mots manquaient à l'appel.

## Transcrire avec faster-whisper sur Runpod Serverless : six tâches mesurées, la facture et un client Go

### Un taxi au compteur, démarrage compris

faster-whisper reprend les modèles Whisper sur un autre moteur d'inférence, CTranslate2. Son dépôt annonce une version « up to 4 times faster than openai/whisper for the same accuracy », avec moins de mémoire[^1]. Ça reste un réseau de neurones qui veut une carte graphique, et c'est précisément ce que je n'ai pas sous la main.

Le mode serverless de Runpod règle ça. On déploie un worker, une image Docker qui sait traiter une tâche, derrière un endpoint, l'adresse qui reçoit les tâches. Quand une tâche arrive et qu'aucun worker ne tourne, Runpod en démarre un sur une machine libre, lui confie la tâche, puis l'arrête quand il n'y a plus rien à faire. Entre deux vidéos, rien ne tourne, et rien n'est facturé.

C'est un taxi au compteur, avec deux subtilités que la documentation énonce honnêtement[^2]. Le compteur démarre quand le chauffeur met le contact, pas quand tu montes : le démarrage à froid, le temps de lancer le conteneur et de charger ce qu'il faut en mémoire, est facturé. Et il s'arrête quand le chauffeur repart, pas quand tu descends : après sa dernière tâche, le worker attend cinq secondes par défaut au cas où une autre arriverait, et ces secondes-là aussi sont payées. Le tout est arrondi à la seconde supérieure. La course la moins chère est celle qu'on enchaîne.

Mon endpoint tourne sur une RTX 4090, comme le détaille [le bilan chiffré de ma chaîne vidéo sur GPU serverless](/guides/gpu-cloud-ia/serverless-vs-pods-gpu/pipeline-video-gpu-serverless-retour-experience/). Le choix des cartes, des fournisseurs et des formules est le sujet du [guide pour louer un GPU dans le cloud](/guides/gpu-cloud-ia/).

### Ce que fait le worker officiel, d'après son code

Je n'ai pas écrit le worker : j'utilise celui que Runpod publie sous le nom worker-faster_whisper. Une tâche est un objet JSON dont le champ `input` porte l'audio et les réglages :

```json
{
  "input": {
    "audio_base64": "UklGRiQ...",
    "model": "large-v3",
    "language": "fr",
    "word_timestamps": true,
    "enable_vad": false,
    "initial_prompt": "CLOUD Act, Schrems II"
  }
}
```

L'audio arrive en base64 (`audio_base64`) ou par une adresse web (`audio`), jamais les deux. `model` choisit entre tiny, base, small, medium, les large et turbo. `transcription` règle le format du texte rendu, brut, SRT ou VTT. `word_timestamps` ajoute l'horodatage de chaque mot, sans lequel Remotion ne sait rien caler[^3].

La lecture du code m'a appris trois choses que le README ne dit pas[^4]. D'abord, tous les modèles sont téléchargés dans l'image quand on la construit, mais aucun n'est chargé au démarrage : la fonction `setup()` ne fait rien, et un modèle n'arrive en mémoire qu'à la première tâche qui le demande. Ce chargement compte donc dans le temps d'exécution de cette tâche, pas dans le démarrage à froid.

Ensuite, le worker ne garde qu'un modèle à la fois. Passer de turbo à large-v3 décharge le premier pour charger le second, et le retour coûte autant que l'aller.

Enfin, il fige faster-whisper à la version 1.1.0, de novembre 2024, alors que la 1.2.1 est sortie fin octobre 2025. Il n'utilise pas non plus le mode par lots, plus rapide, que détaille [la page du guide consacrée à faster-whisper](/guides/gpu-cloud-ia/audio-ia-gpu-cloud/faster-whisper-gpu-cloud-transcription/) : il lit le fichier d'un bout à l'autre, par fenêtres de 30 secondes.

### Six tâches, trois réveils

Pour mesurer, j'ai pris quatre voix off d'une de mes vidéos, mises bout à bout avec ffmpeg : 99 secondes de français, en 16 kHz mono, la fréquence à laquelle faster-whisper ramène de toute façon l'audio. Le WAV pèse 3,2 Mo, 4,2 Mo une fois encodé en base64, sous la limite de 10 Mo d'une requête `/run`[^5]. Ce sont des voix de synthèse : j'ai donc le texte exact de ce qu'elles disent, 291 mots, de quoi juger la transcription au mot près.

Six tâches, envoyées une à une le 3 novembre 2026 entre 14 h 06 et 14 h 09, heure de Paris, par le client Go décrit plus loin :

| # | Modèle | Situation du worker | Attente (`delayTime`) | Exécution (`executionTime`) | Total vu du client |
|---|---|---|---|---|---|
| 1 | turbo | aucun worker allumé | 10 759 ms | 5 970 ms | 19,6 s |
| 2 | turbo | allumé, modèle chargé | 72 ms | 3 003 ms | 5,1 s |
| 3 | large-v3 | arrêté depuis quelques secondes | 3 119 ms | 9 692 ms | 15,5 s |
| 4 | large-v3 | allumé, modèle chargé | 106 ms | 5 464 ms | 8,2 s |
| 5 | large-v3 et texte d'amorce | arrêté depuis une cinquantaine de secondes | 54 441 ms | 10 378 ms | 67,5 s |
| 6 | turbo et VAD | allumé, changement de modèle | 73 ms | 6 096 ms | 8,2 s |

`delayTime` mesure le temps passé dans la file, démarrage à froid compris, et `executionTime` le travail du worker une fois la tâche en main, les deux en millisecondes[^6]. La colonne de droite ajoute l'envoi des 4,2 Mo et l'intervalle d'interrogation du client.

La tâche 2 donne le prix plancher : 3 secondes de calcul pour 99 secondes d'audio avec turbo, 33 fois plus vite que la lecture, et 72 millisecondes d'attente. La tâche 4 fait de même pour large-v3 : 5,5 secondes, 18 fois plus vite que la lecture. Les tâches 1 et 3 coûtent 3 et 4 secondes de plus que leurs jumelles : c'est le chargement du modèle, payé dans le temps d'exécution, comme le code le laissait prévoir.

Le démarrage à froid, lui, n'est pas un chiffre. Entre les tâches 2 et 3, j'ai laissé passer environ huit secondes, trois de trop : le worker s'était arrêté, et son réveil a pris 3,1 secondes, sans doute grâce à FlashBoot, l'option de Runpod qui garde l'état d'un worker arrêté depuis peu[^6]. Le premier réveil de la séance en a pris 10,8. Celui de la tâche 5, une cinquantaine de secondes après la tâche 4, en a pris 54,4. Le 27 avril, le même endpoint avait mis 9,3 secondes à se réveiller, puis 2,6 secondes à transcrire un fichier de 5 secondes avec le modèle base.

![Illustration de trois machines de laiton alignées dans une salle de serveurs sombre, de moins en moins éclairées à mesure qu'elles s'éloignent, avec un sablier au premier plan](/images/whisper-runpod-transcription-gpu-loue-demarrage-froid.original.webp "Trois réveils du même worker en trois minutes : 11 secondes, puis 3, puis 54.")

Un réveil de 3 à 54 secondes pour le même service, dans la même séance : on ne le budgète pas, on le borne. Pour des sous-titres, une minute d'attente ne gêne personne. Pour une application qui attend la réponse devant un utilisateur, c'est une autre histoire, et la seule parade complète est un worker actif en permanence, payé même quand il ne fait rien.

### Turbo ou large-v3 : un mot de travers et une phrase inventée

Le turbo, arrivé avec la version de Whisper du 30 septembre 2024, est une version optimisée de large-v3 : 809 millions de paramètres au lieu de 1 550, environ huit fois plus rapide dans l'implémentation d'origine, pour une précision à peine moindre[^7]. Sur mon extrait, turbo ne va même pas deux fois plus vite, et l'écart de précision tient en quelques mots.

| Tâche | Écarts avec le texte d'origine (291 mots) |
|---|---|
| large-v3 | « Schrems 2 » pour « Schrems deux », et un point de trop au milieu d'une phrase |
| turbo | « Schrems II », et quatre mots jamais prononcés à la fin : « La Cour de justice » |
| turbo et VAD | « Schrems II », rien d'inventé |
| large-v3 et texte d'amorce | la même transcription que large-v3, à une virgule près |

Les deux modèles écrivent « 2018 » là où la voix dit « deux mille dix-huit », ce qui est exactement ce qu'on veut dans un sous-titre. Et le « Schrems II » de turbo est le nom sous lequel tout le monde connaît l'arrêt de 2020 : sur ce mot, le modèle a raison contre mon texte.

Les quatre mots inventés, eux, tiennent dans les 0,22 dernières secondes du fichier, entre 98,80 et 99,02 secondes, la queue du fichier, après le dernier mot. Le segment affiche une log-probabilité moyenne de -0,82, contre -0,08 à -0,10 pour tout le reste : le modèle lui-même n'y croyait pas. Avec `enable_vad`, qui fait passer l'audio par le détecteur de voix Silero avant la transcription[^3], la phrase fantôme disparaît. La tâche 6 a pris 6,1 secondes, rechargement du modèle compris.

Le texte d'amorce (`initial_prompt`) n'a rien changé. Je lui ai donné « CLOUD Act, Schrems II, DSI, Google Drive. » en espérant la bonne casse : large-v3 a rendu « Cloud Act » et « Schrems 2 », comme sans amorce. Le README le décrit comme un texte fourni pour la première fenêtre : une suggestion, pas un dictionnaire. En mai, ce même modèle avait d'ailleurs transcrit la première voix, seule, en « CLUD Act ». Le 3 novembre, avec trois voix autour, il a écrit « Cloud Act ». Comme mes voix sont synthétiques, un second script recale de toute façon le texte de Whisper sur le script de la voix : je garde de Whisper le minutage, et du script les mots.

### Le client Go : `/run`, `/status` et un cache

Mon script de production est en Python. Il soumet chaque voix avec `/run`, quatre tâches en parallèle, interroge `/status/{id}` toutes les dix secondes et range chaque réponse brute dans un dossier de cache. Pour cet article, je l'ai réécrit en Go, sans rien d'autre que la bibliothèque standard, en gardant ces trois idées.

`/run` répond tout de suite avec un identifiant de tâche. `/runsync` garderait la connexion ouverte jusqu'au résultat, mais ne conserve celui-ci qu'une minute, contre 30 pour `/run`[^5]. Pour un long fichier ou une file d'attente lente, l'asynchrone est plus sûr.

```go
// input reprend les champs du worker officiel worker-faster_whisper.
type input struct {
	AudioBase64    string `json:"audio_base64"`
	Model          string `json:"model"`
	Language       string `json:"language,omitempty"`
	WordTimestamps bool   `json:"word_timestamps"`
	EnableVAD      bool   `json:"enable_vad"`
	InitialPrompt  string `json:"initial_prompt,omitempty"`
}

// job est la réponse de /run et de /status.
type job struct {
	ID            string          `json:"id"`
	Status        string          `json:"status"`
	DelayTime     int64           `json:"delayTime"`     // millisecondes
	ExecutionTime int64           `json:"executionTime"` // millisecondes
	Error         json.RawMessage `json:"error"`
	Output        json.RawMessage `json:"output"`
}

type client struct {
	http *http.Client
	key  string
	base string // https://api.runpod.ai/v2/<endpoint>
}

func (c client) call(ctx context.Context, method, path string, body any) (job, error) {
	var j job
	var r io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return j, err
		}
		r = bytes.NewReader(b)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.base+path, r)
	if err != nil {
		return j, err
	}
	req.Header.Set("Authorization", "Bearer "+c.key)
	req.Header.Set("Content-Type", "application/json")
	resp, err := c.http.Do(req)
	if err != nil {
		return j, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return j, fmt.Errorf("%s %s : %s %s", method, path, resp.Status, msg)
	}
	return j, json.NewDecoder(resp.Body).Decode(&j)
}

// transcribe soumet la tâche avec /run, puis interroge /status chaque seconde.
func (c client) transcribe(ctx context.Context, in input) (job, error) {
	j, err := c.call(ctx, http.MethodPost, "/run", map[string]input{"input": in})
	if err != nil {
		return j, err
	}
	id := j.ID
	for {
		switch j.Status {
		case "COMPLETED":
			return j, nil
		case "FAILED", "CANCELLED", "TIMED_OUT":
			return j, fmt.Errorf("tâche %s : %s %s", id, j.Status, j.Error)
		}
		select {
		case <-ctx.Done():
			return j, ctx.Err()
		case <-time.After(time.Second):
		}
		if j, err = c.call(ctx, http.MethodGet, "/status/"+id, nil); err != nil {
			return j, err
		}
	}
}
```

`Output` reste du JSON brut, pour une raison simple : c'est lui qu'on met en cache. Dans `main`, le programme cherche d'abord la réponse sur le disque, et ne paie le GPU que s'il ne la trouve pas :

```go
	// Le cache : la réponse brute, pour ne jamais payer deux fois le même fichier.
	cache := *out + ".json"
	raw, err := os.ReadFile(cache)
	if err != nil {
		audio, err := os.ReadFile(flag.Arg(0))
		if err != nil {
			log.Fatal(err)
		}
		c := client{
			http: &http.Client{Timeout: time.Minute},
			key:  os.Getenv("RUNPOD_API_KEY"),
			base: "https://api.runpod.ai/v2/" + os.Getenv("RUNPOD_ENDPOINT_ID"),
		}
		// ... appel de c.transcribe, journal des deux durées ...
		raw = j.Output
		if err := os.WriteFile(cache, raw, 0o644); err != nil {
			log.Fatal(err)
		}
	}
```

La clé et l'identifiant de l'endpoint viennent de deux variables d'environnement, jamais du code. Les six tâches du tableau sont passées par ce programme, compilé et exécuté avec Go 1.27.3 sous Linux. Interroger chaque seconde plutôt que toutes les dix change le temps vu du client, 5 secondes au lieu d'un peu plus de 10 pour la tâche 2, mais pas la facture : le GPU ne tourne pas pendant que le client attend.

### Le FBI a disparu des sous-titres

Restait à écrire le SRT. J'ai commencé par relire comment mon script Python le fait, et c'est là que le défaut est apparu :

```python
def wrap_text(text: str, line_max: int = SRT_LINE_MAX) -> str:
    words = text.split()
    lines, cur = [], ""
    for w in words:
        candidate = (cur + " " + w).strip()
        if len(candidate) > line_max and cur:
            lines.append(cur)
            cur = w
        else:
            cur = candidate
    if cur:
        lines.append(cur)
    return "\n".join(lines[:2])  # max 2 lignes par sous-titre
```

Le script fait un sous-titre par segment de Whisper, le coupe en lignes de 42 caractères, la longueur que Netflix conseille de ne pas dépasser pour la plupart des langues[^8], puis garde deux lignes. Les autres partent, sans un mot dans le journal. Or rien n'oblige un segment de Whisper à tenir en 84 caractères : sur la vidéo d'où vient l'extrait, le plus long en compte 221.

J'ai recompté les sous-titres de cette vidéo : 34 sur 82 sont tronqués, et 165 mots sur 1 073 manquent, soit 15 %. Le troisième s'arrête sur « pourrait techniquement intéresser ». Intéresser qui ? Le FBI, que Whisper avait parfaitement entendu, et que le script a jeté. La même fonction est copiée dans le second script, celui qui corrige le texte : le défaut survit à la correction. J'ai réparé les deux scripts depuis : un sous-titre trop long en devient plusieurs, et les 1 073 mots sont revenus dans le fichier.

![Illustration d'un ruban ambré couvert de lignes abstraites qui sort d'une machine de laiton sur un établi sombre, sous une lampe, à côté d'une corbeille](/images/whisper-runpod-transcription-gpu-loue-sous-titres-tronques.original.webp "Deux lignes de 42 caractères au plus, et le reste à la corbeille : le découpage de mon script Python, avant correction.")

Plus les segments s'allongent, plus la perte grandit. Sur l'extrait du 3 novembre, la même fonction aurait perdu 34 mots sur 287 avec la sortie de large-v3. Avec turbo et la VAD, qui regroupe la parole en 8 longs segments au lieu de 23, elle en aurait perdu 187. Le réglage qui supprime la phrase inventée aurait fait disparaître les deux tiers du texte.

La correction consiste à ne plus partir des segments, mais des mots horodatés. Chaque sous-titre se remplit mot par mot, et un sous-titre plein en ouvre un autre au lieu de jeter la suite :

```go
// joinPieces recolle les morceaux de mots : Whisper renvoie « d » puis
// « 'équipe ». Seul un morceau qui commence par une espace ouvre un mot.
func joinPieces(pieces []word) []word {
	var out []word
	for _, p := range pieces {
		if n := len(out); n > 0 && !strings.HasPrefix(p.Word, " ") {
			out[n-1].Word += p.Word
			out[n-1].End = p.End
			continue
		}
		out = append(out, p)
	}
	return out
}

// cues range les mots en sous-titres de deux lignes au plus.
// Un sous-titre plein en ouvre un autre : aucun mot ne se perd.
func cues(pieces []word) []cue {
	var out []cue
	var cur cue
	for _, w := range joinPieces(pieces) {
		t := strings.TrimSpace(w.Word)
		if t == "" {
			continue
		}
		n := len(cur.lines)
		switch {
		case n == 0:
			cur = cue{start: w.Start, lines: []string{t}}
		case utf8.RuneCountInString(cur.lines[n-1])+1+utf8.RuneCountInString(t) <= maxLine:
			cur.lines[n-1] += " " + t
		case n < 2:
			cur.lines = append(cur.lines, t)
		default:
			out = append(out, cur)
			cur = cue{start: w.Start, lines: []string{t}}
		}
		cur.end = w.End
		// Une fin de phrase ferme le sous-titre en cours.
		if last := t[len(t)-1]; last == '.' || last == '?' || last == '!' {
			out = append(out, cur)
			cur = cue{}
		}
	}
	if len(cur.lines) > 0 {
		out = append(out, cur)
	}
	return out
}
```

`joinPieces` n'existait pas dans ma première version, qui passait pourtant mon test, écrit avec des mots entiers. Rejouée sur une vraie réponse de l'endpoint, gardée en cache depuis mai, elle a écrit « d 'équipe » dans le SRT. Whisper renvoie des morceaux, pas des mots : seul l'espace en tête d'un morceau annonce un mot nouveau. Sur les 1 135 morceaux de la vidéo, 62 n'en ont pas, des élisions pour la plupart.

Le test qui protège tout ça vérifie une règle plutôt qu'une liste d'exemples, comme un [test par propriétés](/blog/property-based-testing-go-rapid/), mais sans bibliothèque. Il tire 2 000 textes au hasard, dont un morceau sur huit se colle au précédent, et vérifie qu'aucun sous-titre n'a plus de deux lignes, qu'aucune ligne de plusieurs mots ne dépasse 42 caractères et que les lignes mises bout à bout redonnent le texte exact :

```go
		if got := strings.Join(lignes, " "); got != strings.TrimSpace(texte) {
			t.Fatalf("texte perdu ou déformé :\n%q\n%q", got, texte)
		}
```

Une fonction qui garde deux lignes et jette le reste échouerait dès le premier texte de plus de 84 caractères, c'est-à-dire presque à chaque tirage.

Le découpage n'est pas parfait pour autant. Sur l'extrait, « le FBI. » a un sous-titre pour lui seul, affiché 0,44 seconde : la ligne précédente était pleine, et la phrase finissait deux mots plus loin. Il est seul et il passe vite, mais il est là.

**DevOps Dave :** J'ai transcrit toutes mes vidéos avec large-v3. J'ai relu le texte, il est parfait.

**Security Sarah :** Le texte du JSON, ou le fichier SRT ?

**DevOps Dave :** C'est le même texte.

**Security Sarah :** Ouvre le sous-titre 3.

**DevOps Dave :** « ...pourrait techniquement intéresser ». Intéresser qui ?

**Security Sarah :** Le FBI. Il est dans ton JSON. Ton script l'a jeté en écrivant le SRT.

### La facture : quelques centimes, et ce qu'ils paient

Les prix serverless de Runpod, relevés le 3 novembre 2026 sur une page mise à jour le 28 octobre, pour les flex workers, ceux qui s'éteignent quand rien ne tourne[^9] :

| Cartes (mémoire) | Prix à l'heure | Prix à la seconde |
|---|---|---|
| A4000, A4500, RTX 4000, RTX 2000 (16 Go) | 0,58 $ | 0,00016 $ |
| L4, A5000, RTX 3090 (24 Go) | 0,75 $ | 0,00021 $ |
| RTX 4090 (24 Go) | 1,34 $ | 0,00037 $ |
| A100 (80 Go) | 2,72 $ | 0,00076 $ |

OpenAI annonce environ 10 Go de mémoire pour large-v3 et 6 Go pour turbo dans son implémentation d'origine, et faster-whisper en demande moins : la plus petite catégorie suffit, et la RTX 4090 de mon endpoint est un luxe de vitesse.

Sur cette RTX 4090, mes six tâches ont totalisé 40,6 secondes d'exécution, soit 1,5 centime. Le reste dépend de ce que Runpod facture des réveils : les 68 secondes d'attente cumulées comprennent le démarrage du conteneur et le chargement, facturés, mais aussi le transfert de l'image vers la machine, qui ne l'est pas[^2]. En ajoutant les trois délais d'inactivité de cinq secondes et les quelques secondes entre deux tâches enchaînées, la séance coûte entre 2,4 et 5 centimes.

À chaud, une heure d'audio demanderait environ 200 secondes de RTX 4090 avec large-v3, soit 7 centimes, et 110 secondes avec turbo, soit 4 centimes. Le 27 avril, les 16 voix de la vidéo complète, 372 secondes d'audio, ont été transcrites en 5 minutes environ avec quatre tâches en parallèle, et facturées 169 secondes, 5 centimes, d'après l'historique cité dans le bilan de la chaîne vidéo. À la vitesse mesurée le 3 novembre, le calcul pur de ces 372 secondes tiendrait en une vingtaine de secondes. La facture paie donc surtout des réveils, des chargements de modèle et des attentes, et quatre tâches parallèles peuvent réveiller plusieurs workers, chacun avec son démarrage et son chargement.

C'est le calcul inverse de celui qui m'a fait [payer un VPS pour héberger ce site](/blog/vps-ou-paas-cout-site-statique/). Un service qui répond jour et nuit se paie au mois. Une carte graphique qui travaille trois minutes par vidéo se paie à la seconde, et la vidéo entière coûte moins qu'un café.

### La partie gratuite

Le GPU loué a fait exactement ce qu'on lui demandait. Il a entendu « le FBI », « Google Drive » et « Schrems » en quelques secondes, pour une fraction de centime, et turbo a même corrigé mon texte sur le nom d'un arrêt. Les erreurs sérieuses de toute la chaîne étaient dans la partie gratuite : quelques lignes de Python qui produisaient un fichier parfaitement valide.

Un SRT tronqué s'ouvre sans erreur, s'affiche sans erreur et ne dit à personne qu'il lui manque 15 % du texte. On paie la carte graphique à la seconde, et le comptage qui révèle ce genre de défaut ne coûte rien. C'est la seule ligne de la facture que j'avais oublié de régler.

---

[^1]: Le [dépôt de faster-whisper](https://github.com/SYSTRAN/faster-whisper), maintenu par SYSTRAN, décrit la bibliothèque comme une réimplémentation de Whisper sur CTranslate2, un moteur d'inférence pour les modèles Transformer, et précise que la quantification sur 8 bits améliore encore son efficacité. Sa fonction `decode_audio`, dans [`faster_whisper/audio.py`](https://github.com/SYSTRAN/faster-whisper/blob/v1.1.0/faster_whisper/audio.py), rééchantillonne l'audio à 16 kHz par défaut.

[^2]: La [page de tarification serverless de Runpod](https://docs.runpod.io/serverless/pricing) : facturation du démarrage du worker à son arrêt complet, arrondie à la seconde supérieure, démarrage à froid et délai d'inactivité compris (cinq secondes par défaut). Le téléchargement de l'image Docker et des modèles en cache pendant que le worker est arrêté n'est pas facturé.

[^3]: Le [README du worker worker-faster_whisper](https://github.com/runpod-workers/worker-faster_whisper) liste tous les champs d'entrée, dont `initial_prompt`, décrit comme le texte fourni pour la première fenêtre, et `enable_vad`, qui filtre les passages sans voix avec le modèle Silero VAD.

[^4]: Le fichier [`src/predict.py`](https://github.com/runpod-workers/worker-faster_whisper/blob/main/src/predict.py) contient le chargement paresseux, la règle d'un seul modèle en mémoire et la liste `AVAILABLE_MODELS`. Le README propose aussi distil-large-v2 et distil-large-v3, que cette liste refuse ; pour le français, la perte est nulle, les modèles Distil-Whisper étant conçus pour l'anglais. La version 1.1.0 de faster-whisper est figée dans `builder/requirements.txt`, et l'[historique des versions sur PyPI](https://pypi.org/project/faster-whisper/#history) date la 1.1.0 du 21 novembre 2024 et la 1.2.1 du 31 octobre 2025.

[^5]: La [référence des opérations Runpod](https://docs.runpod.io/serverless/endpoints/operation-reference) fixe la taille maximale d'une requête à 10 Mo pour `/run` et à 20 Mo pour `/runsync`. La [page sur l'envoi des requêtes](https://docs.runpod.io/serverless/endpoints/send-requests) indique la durée de conservation des résultats : 30 minutes pour `/run`, une minute pour `/runsync`.

[^6]: La [page d'optimisation de Runpod](https://docs.runpod.io/serverless/development/optimization) découpe le temps d'attente en temps de file et en démarrage à froid. La [page des réglages d'un endpoint](https://docs.runpod.io/serverless/endpoints/endpoint-configurations) décrit FlashBoot, qui conserve l'état d'un worker après son arrêt pour le relancer plus vite, le délai d'inactivité de cinq secondes par défaut dans la console, et les workers actifs, qui suppriment les démarrages à froid mais sont facturés en continu, même sans tâche.

[^7]: Le [README de Whisper](https://github.com/openai/whisper) donne la taille, la mémoire et la vitesse relative de chaque modèle, précise que turbo n'est pas entraîné pour la traduction, et explique que `transcribe()` lit l'audio par une fenêtre glissante de 30 secondes. Le [journal des versions](https://github.com/openai/whisper/blob/main/CHANGELOG.md) date l'arrivée de large-v3-turbo de la version v20240930.

[^8]: La [page d'aide de Netflix sur la longueur des lignes](https://partnerhelp.netflixstudios.com/hc/en-us/articles/215274938-What-is-the-maximum-number-of-characters-per-line-allowed-in-Timed-Text-assets) : la plupart des langues n'ont pas de limite contrôlée automatiquement, mais s'en tenir à environ 42 caractères par ligne évite qu'une ligne trop proche du bord de l'image s'affiche mal.

[^9]: La [page des prix de Runpod](https://www.runpod.io/pricing), section Serverless, porte la mention « Updated October 28, 2026 ». Les prix à la seconde sont ceux qu'elle affiche, arrondis à cinq décimales.



