jer3mi commited on
Commit
efe0ef4
·
verified ·
1 Parent(s): b1872be

loudkit 0.1.1: two models, 28 voices, released revisions

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.gitattributes CHANGED
@@ -33,3 +33,21 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ audio/loudr-1/clara.opus filter=lfs diff=lfs merge=lfs -text
37
+ audio/loudr-1/colette.opus filter=lfs diff=lfs merge=lfs -text
38
+ audio/loudr-1/emma.opus filter=lfs diff=lfs merge=lfs -text
39
+ audio/loudr-1/henri.opus filter=lfs diff=lfs merge=lfs -text
40
+ audio/loudr-1/henry.opus filter=lfs diff=lfs merge=lfs -text
41
+ audio/loudr-1/lucy.opus filter=lfs diff=lfs merge=lfs -text
42
+ audio/loudr-1/miles.opus filter=lfs diff=lfs merge=lfs -text
43
+ audio/loudr-1/oliver.opus filter=lfs diff=lfs merge=lfs -text
44
+ audio/loudr-1/oscar.opus filter=lfs diff=lfs merge=lfs -text
45
+ audio/loudr-1/sophie.opus filter=lfs diff=lfs merge=lfs -text
46
+ audio/turbo/clara.opus filter=lfs diff=lfs merge=lfs -text
47
+ audio/turbo/emma.opus filter=lfs diff=lfs merge=lfs -text
48
+ audio/turbo/henry.opus filter=lfs diff=lfs merge=lfs -text
49
+ audio/turbo/lucy.opus filter=lfs diff=lfs merge=lfs -text
50
+ audio/turbo/miles.opus filter=lfs diff=lfs merge=lfs -text
51
+ audio/turbo/oliver.opus filter=lfs diff=lfs merge=lfs -text
52
+ audio/turbo/oscar.opus filter=lfs diff=lfs merge=lfs -text
53
+ audio/turbo/sophie.opus filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -9,56 +9,48 @@ python_version: "3.12.12"
9
  app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
- short_description: On-device TTS. Twenty voices, ten languages, one engine.
13
  models:
14
  - loudreader/loudr-1
 
15
  preload_from_hub:
16
- - loudreader/loudr-1 loudr-1.safetensors,loudr-1-enrollment.safetensors,ve.safetensors,manifest.json,release.json,SHA256SUMS,voices/carmen.safetensors,voices/colette.safetensors,voices/dante.safetensors,voices/darkman.safetensors,voices/dave.safetensors,voices/freja.safetensors,voices/gosia.safetensors,voices/henri.safetensors,voices/ines.safetensors,voices/joe.safetensors,voices/kathleen.safetensors,voices/kerstin.safetensors,voices/nathalie.safetensors,voices/nils.safetensors,voices/paola.safetensors,voices/pim.safetensors,voices/selma.safetensors,voices/soren.safetensors,voices/thorsten.safetensors,voices/tugao.safetensors
 
17
  ---
18
 
19
  # loudkit
20
 
21
- Twenty voices across ten languages, from [loudreader/loudr-1](https://huggingface.co/loudreader/loudr-1),
 
 
22
  running the [loudkit](https://github.com/loudreader/loudkit) engine on ZeroGPU.
23
 
24
  ## Two tabs
25
 
26
- - **Voices.** Twenty voices, each beside the reference recording it was enrolled
27
- from. English first, with a dropdown for the other nine languages. The samples
28
- were rendered ahead of time and ship in this repo, so playing them uses no GPU.
29
- Under them is a box for your own text, up to 1,000 characters.
30
- - **Clone.** A voice from about ten seconds of audio. Try a shipped example, or
31
- record yourself.
 
 
 
 
32
 
33
- ZeroGPU bills GPU time to the visitor, not to the owner. An anonymous visitor
34
- gets about two minutes a day. A signed-in free account gets about five. Listening
35
- costs none of it.
36
 
37
- ## Cloning and consent
 
 
38
 
39
- Clone your own voice, or a voice you have permission to use.
 
40
 
41
- - Four shipped examples let you hear cloning without recording anything. Their
42
- donors released them for building TTS voices.
43
- - Recording yourself is the next path. It clears the example.
44
- - An upload is last, and needs an explicit confirmation.
45
- - Recordings are deleted when the request ends. The voice embeddings are never
46
- written to disk and never leave the process.
47
- - Cloned audio carries no download button.
48
 
49
- See [RESPONSIBLE_USE](https://github.com/loudreader/loudkit/blob/main/RESPONSIBLE_USE.md).
50
-
51
- ## Determinism
52
-
53
- The Voices tab has a determinism check. It renders the same text twice at the same
54
- seed and prints the SHA-256 of both waveforms. They match.
55
-
56
- That holds within this build and this device. loudkit promises a bit-identical
57
- waveform for the same seed, build, backend and input. It does not promise that
58
- your machine matches this GPU. See the
59
- [identity contract](https://github.com/loudreader/loudkit/blob/main/docs/reference/IDENTITY-CONTRACT.md).
60
-
61
- ## Run it locally
62
 
63
  ```bash
64
  pip install "loudkit[torch,audio,enroll,hub]"
@@ -67,16 +59,9 @@ pip install "loudkit[torch,audio,enroll,hub]"
67
  ```python
68
  import loudkit as lk
69
 
70
- engine = lk.load("loudreader/loudr-1")
71
- voice = lk.voice("kathleen", repo="loudreader/loudr-1")
72
  engine.synthesize_long("Hello from loudkit.", voice, seed=7).save("hello.wav")
73
  ```
74
 
75
- Audio in this Space, and from `Result.save`, carries C2PA provenance: the
76
- algorithm fingerprint, the recipe and the seed.
77
-
78
- ## Voice sources
79
-
80
- Every voice is enrolled from a public-domain or openly licensed recording.
81
- `voices.json` in this repo carries the full record for each one: donor, source,
82
- licence, consent, and the SHA-256 of both the reference and the sample.
 
9
  app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
+ short_description: On-device TTS. 28 voices, ten languages, two models.
13
  models:
14
  - loudreader/loudr-1
15
+ - loudreader/loudr-1-turbo
16
  preload_from_hub:
17
+ - loudreader/loudr-1 loudr-1.safetensors,loudr-1-enrollment.safetensors,ve.safetensors,tokenizer.json,manifest.json,release.json,SHA256SUMS,voices/carmen.safetensors,voices/clara.safetensors,voices/colette.safetensors,voices/dante.safetensors,voices/darkman.safetensors,voices/dave.safetensors,voices/emma.safetensors,voices/freja.safetensors,voices/gosia.safetensors,voices/henri.safetensors,voices/henry.safetensors,voices/ines.safetensors,voices/joe.safetensors,voices/kathleen.safetensors,voices/kerstin.safetensors,voices/lucy.safetensors,voices/miles.safetensors,voices/nathalie.safetensors,voices/nils.safetensors,voices/oliver.safetensors,voices/oscar.safetensors,voices/paola.safetensors,voices/pim.safetensors,voices/selma.safetensors,voices/sophie.safetensors,voices/soren.safetensors,voices/thorsten.safetensors,voices/tugao.safetensors
18
+ - loudreader/loudr-1-turbo loudr-1-turbo.safetensors,tokenizer.json,manifest.json,release.json,SHA256SUMS
19
  ---
20
 
21
  # loudkit
22
 
23
+ Twenty-eight voices across ten languages, on two models:
24
+ [loudreader/loudr-1](https://huggingface.co/loudreader/loudr-1) and
25
+ [loudreader/loudr-1-turbo](https://huggingface.co/loudreader/loudr-1-turbo),
26
  running the [loudkit](https://github.com/loudreader/loudkit) engine on ZeroGPU.
27
 
28
  ## Two tabs
29
 
30
+ - **Voices.** Twenty-eight voices, each beside the reference recording it was
31
+ enrolled from, and each rendered on both models from the same passage at the
32
+ same seed. English first, with a dropdown for the other nine languages. The
33
+ samples were rendered ahead of time and ship in this repo, so playing them
34
+ uses no GPU. Under them is a box for your own text, up to 1,000 characters,
35
+ and one that renders the same words on both models side by side.
36
+ - **Clone.** A voice from about ten seconds of a recording, then speak with it
37
+ on either model. One profile serves both, which is what a portable voice
38
+ profile means. The microphone is the default path; an upload is gated on a
39
+ confirmation, and neither recording outlives the request.
40
 
41
+ ## What this demo is honest about
 
 
42
 
43
+ ZeroGPU bills GPU time to the visitor. Listening costs nothing, because the
44
+ fifty-six samples are files. Speaking and cloning spend the visitor's own daily
45
+ quota, which is why the text boxes are capped well below what the library takes.
46
 
47
+ Both models are pinned to an immutable Hub commit rather than to a branch, so
48
+ the fingerprint the page prints is the fingerprint of the bytes it ran.
49
 
50
+ Output files carry C2PA provenance: the algorithm fingerprint, the recipe and
51
+ the seed.
 
 
 
 
 
52
 
53
+ ## Running it locally instead
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  ```bash
56
  pip install "loudkit[torch,audio,enroll,hub]"
 
59
  ```python
60
  import loudkit as lk
61
 
62
+ engine = lk.load("loudreader/loudr-1", revision="v0.1.1")
63
+ voice = lk.voice("kathleen", repo="loudreader/loudr-1", revision="v0.1.1")
64
  engine.synthesize_long("Hello from loudkit.", voice, seed=7).save("hello.wav")
65
  ```
66
 
67
+ Nothing is queued and nothing is metered.
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,24 +1,32 @@
1
- """The loudkit demo Space: hear twenty voices, speak your own text, clone your own.
2
 
3
  ZeroGPU bills GPU time to the *visitor*, not to the owner: an anonymous visitor
4
  gets about two minutes a day, a signed-in free account about five. A demo whose
5
  first click spends that budget is one most people bounce off before they have
6
- heard anything at all. So the Listen tab is twenty pre-rendered files served
7
- straight out of this repo — no GPU, no queue, no quota — and the GPU is spent
8
- only on what a visitor types or records.
9
-
10
- The engine and the enroller are both built on `cuda` at module level, which is
11
- what ZeroGPU asks for: CUDA transfers are optimised for start-up placement, and
12
- lazy-loading inside a `@spaces.GPU` function is explicitly discouraged. Each
13
- decorated call then runs in a freshly forked, short-lived process, which is also
14
- why there is no `torch.compile` and no CUDA graph capture here: both pay their
15
- cost once per process and would never amortise.
16
-
17
- Cloning is exposed, which the CPU scaffold this replaces deliberately did not do.
18
- The reasoning that kept it out was about consent, not about capability, so the
19
- consent is built into the shape of the tab rather than written beside it: the
20
- microphone is the default path, an upload is secondary and gated on an explicit
21
- confirmation, and neither recording outlives the request that carried it.
 
 
 
 
 
 
 
 
22
  """
23
 
24
  from __future__ import annotations
@@ -43,10 +51,29 @@ import loudkit as lk
43
  from loudkit.backends.torch_backend import build_torch_enroller
44
  from loudkit.hub import resolve_enrollment_checkpoint, resolve_voice_encoder
45
 
46
- REPO = "loudreader/loudr-1"
47
  DEVICE = "cuda"
48
  HERE = Path(__file__).parent
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # The CPU scaffold capped text at 300 characters because CPU synthesis ran at
51
  # roughly a tenth of real time. On a GPU the cap is about the visitor's daily
52
  # quota instead, which is a far looser bound: 1000 characters is ~70 s of speech.
@@ -66,14 +93,14 @@ RESPONSIBLE_USE = f"{DOCS}/blob/main/RESPONSIBLE_USE.md"
66
  ROSTER = json.loads((HERE / "voices.json").read_text(encoding="utf-8"))
67
  BY_NAME = {entry["name"]: entry for entry in ROSTER}
68
  ORDERED = sorted(ROSTER, key=lambda e: (e["language"], e["name"]))
69
- VOICE_CHOICES = [(f"{e['name']} · {e['language']} ({e['gender']})", e["name"]) for e in ORDERED]
70
 
71
  # English leads. A visitor who does not read the other nine should not have to
72
  # hunt for the one they do, and alphabetical order put Danish first.
73
  FIRST_LANGUAGE = "en"
74
  _LANG_OF = {e["language_id"]: e["language"] for e in ROSTER}
75
  LANGUAGE_FILTER = [(_LANG_OF[FIRST_LANGUAGE], FIRST_LANGUAGE)] + [
76
- (name, code) for code, name in sorted(_LANG_OF.items(), key=lambda kv: kv[1])
 
77
  if code != FIRST_LANGUAGE
78
  ]
79
 
@@ -101,24 +128,32 @@ EXAMPLE_CHOICES = [("Nothing selected", "")] + [
101
  # Module-level model placement, per the ZeroGPU contract.
102
  # --------------------------------------------------------------------------
103
 
104
- engine = lk.load(REPO, device=DEVICE)
 
 
 
105
 
106
  # Voice profiles are numpy, not torch, so they are device-agnostic and cost a
107
- # few hundred kilobytes each. Loading all twenty up front means switching voice
108
- # in the Speak tab never blocks on a download.
109
- PROFILES = {entry["name"]: lk.voice(entry["name"], repo=REPO) for entry in ROSTER}
 
 
 
 
110
 
111
  # Enrollment reads the other half of the release: the speech tokenizer and the
112
  # speaker encoder, which synthesis never touches, plus the utterance voice
113
- # encoder that sits beside both. `lk.enroll()` builds this per call by design;
114
- # a Space would pay the load on every clone, so it is built once here instead.
 
115
  enroller = build_torch_enroller(
116
- str(resolve_enrollment_checkpoint(REPO)),
117
  device=DEVICE,
118
- voice_encoder_weights=str(resolve_voice_encoder(REPO)),
119
  )
120
 
121
- FINGERPRINT = engine.algorithm.fingerprint()
122
 
123
  _LANGUAGE_NAMES = {e["language_id"]: e["language"] for e in ROSTER}
124
  LANGUAGE_CHOICES = [("Follow the voice", "")] + [
@@ -153,11 +188,11 @@ def _write(result: lk.Result, *, voice: str, language: str) -> str:
153
  return out.name
154
 
155
 
156
- def _stats(result: lk.Result) -> str:
157
  seconds = len(result.audio) / result.sample_rate
158
  return (
159
  f"**{seconds:.1f} s of audio.** {result.timings.describe(seconds)}\n\n"
160
- f"`algo[{result.algorithm_fingerprint}]` · seed `{result.seed}` · "
161
  f"speed `{result.speed:g}x` · {result.sample_rate} Hz"
162
  )
163
 
@@ -183,21 +218,26 @@ def _check(text: str, limit: int) -> str:
183
  return text
184
 
185
 
 
 
 
 
186
  # --------------------------------------------------------------------------
187
  # Listen. No GPU: these files were rendered ahead of time and ship in the repo.
188
  # --------------------------------------------------------------------------
189
 
190
 
191
- def listen(name: str):
192
  entry = BY_NAME[name]
193
  sample, reference, source = entry["sample"], entry["reference"], entry["source"]
 
194
 
195
  lines = [
196
  f"### {entry['name']}. {entry['language']} ({entry['gender']}).",
197
  "",
198
  f"> {sample['text']}",
199
  "",
200
- f"From *{sample['work']}*, seed `{sample['seed']}`.",
201
  "",
202
  f"- Reference recording: {reference['duration_s']:.1f} s, {reference['construction']}.",
203
  f"- Source: [{source['name']}]({source['url']}), {source['license']}.",
@@ -206,10 +246,10 @@ def listen(name: str):
206
  similarity = entry.get("speaker_similarity")
207
  if similarity is not None:
208
  lines.append(f"- Speaker similarity to the reference: {similarity:.3f}.")
209
- lines.append(f"- Voice profile: `{entry['profile']['hf_path']}`.")
210
 
211
  return (
212
- str(HERE / sample["audio"]),
213
  str(HERE / reference["public_preview"]),
214
  "\n".join(lines),
215
  )
@@ -232,14 +272,14 @@ ROSTER_TABLE = [
232
  # --------------------------------------------------------------------------
233
 
234
 
235
- def _speak_duration(text, name, language, seed, speed):
236
  return _estimate(text, overhead=15.0)
237
 
238
 
239
  @spaces.GPU(duration=_speak_duration)
240
- def speak(text: str, name: str, language: str, seed: float, speed: float):
241
  text = _check(text, MAX_CHARS)
242
- result = engine.synthesize_long(
243
  text,
244
  PROFILES[name],
245
  seed=int(seed),
@@ -247,7 +287,46 @@ def speak(text: str, name: str, language: str, seed: float, speed: float):
247
  speed=float(speed),
248
  )
249
  label = language or BY_NAME[name]["language_id"]
250
- return _write(result, voice=name, language=label), _stats(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
 
253
  # --------------------------------------------------------------------------
@@ -255,14 +334,14 @@ def speak(text: str, name: str, language: str, seed: float, speed: float):
255
  # --------------------------------------------------------------------------
256
 
257
 
258
- def _clone_duration(example, mic, upload, consent, text, language, seed, speed):
259
  # Enrollment is a fixed cost on top of the render: two encoders and a
260
  # tokenizer over at most 20 s of audio.
261
  return _estimate(text, overhead=30.0)
262
 
263
 
264
  @spaces.GPU(duration=_clone_duration)
265
- def clone(example, mic, upload, consent: bool, text: str, language: str, seed, speed):
266
  """Enroll a voice, speak with it, keep nothing.
267
 
268
  Three ways in, and they do not carry the same consent story, so they are
@@ -315,10 +394,10 @@ def clone(example, mic, upload, consent: bool, text: str, language: str, seed, s
315
  # and read its text through the English funnel.
316
  profile = dataclasses.replace(profile, language=language or "en")
317
 
318
- result = engine.synthesize_long(
319
  text, profile, seed=int(seed), language=language or None, speed=float(speed)
320
  )
321
- return _write(result, voice=label, language=profile.language), _stats(result)
322
  finally:
323
  # Nothing the visitor recorded outlives the request that carried it.
324
  # A shipped example is not the visitor's and is not ours to delete.
@@ -332,13 +411,14 @@ def clone(example, mic, upload, consent: bool, text: str, language: str, seed, s
332
  # --------------------------------------------------------------------------
333
 
334
 
335
- def _probe_duration(text, name, seed):
336
  return _estimate(text, passes=2, overhead=20.0)
337
 
338
 
339
  @spaces.GPU(duration=_probe_duration)
340
- def probe(text: str, name: str, seed: float):
341
  text = _check(text, MAX_PROBE_CHARS)
 
342
  profile = PROFILES[name]
343
  first = engine.synthesize_long(text, profile, seed=int(seed))
344
  second = engine.synthesize_long(text, profile, seed=int(seed))
@@ -353,12 +433,13 @@ def probe(text: str, name: str, seed: float):
353
  "```",
354
  f"render 1 sha256 {left}",
355
  f"render 2 sha256 {right}",
356
- f" algo[{first.algorithm_fingerprint}] seed {int(seed)}",
357
  "```",
358
  "",
359
  "Identical within this build and this device. loudkit promises a "
360
  "bit-identical waveform for the same seed, build, backend and input. "
361
- "It does not promise that your laptop matches this GPU. "
 
362
  f"[Read the identity contract]({IDENTITY_CONTRACT}).",
363
  ]
364
  )
@@ -416,14 +497,17 @@ THEME = gr.themes.Soft(
416
  with gr.Blocks(title="loudkit", theme=THEME, css=CSS, js=FORCE_LIGHT, fill_width=False) as demo:
417
  gr.Markdown(
418
  f"""
419
- # Twenty voices. Ten languages. One engine.
420
 
421
  On-device text to speech, running here on ZeroGPU.
422
- [Model](https://huggingface.co/{REPO}) · [Code]({DOCS}) · [Responsible use]({RESPONSIBLE_USE})
 
 
423
 
424
  <span class="lk-pill">Listening costs no GPU</span>
425
  <span class="lk-pill">Speaking and cloning spend your daily quota</span>
426
- <span class="lk-pill">algo[{FINGERPRINT}]</span>
 
427
  """,
428
  elem_id="lk-head",
429
  )
@@ -432,18 +516,23 @@ On-device text to speech, running here on ZeroGPU.
432
  # ------------- Voices: listen for free, then type your own -------------
433
  with gr.Tab("Voices"):
434
  gr.Markdown(
435
- "Pick a voice and the sample plays at once. Those are rendered "
436
- "ahead of time and use no GPU. Type your own text underneath."
 
437
  )
438
  with gr.Row():
439
  with gr.Column(scale=1):
 
 
 
 
 
 
440
  with gr.Row():
441
  lang_pick = gr.Dropdown(
442
  LANGUAGE_FILTER, value=FIRST_LANGUAGE, label="Language"
443
  )
444
- pick = gr.Dropdown(
445
- FIRST_VOICES, value=FIRST_VOICE, label="Voice"
446
- )
447
  made = gr.Audio(label="loudkit", type="filepath", interactive=False)
448
  ref = gr.Audio(
449
  label="Reference recording", type="filepath", interactive=False
@@ -484,22 +573,49 @@ On-device text to speech, running here on ZeroGPU.
484
  say_stats = gr.Markdown()
485
 
486
  say_go.click(
487
- speak, [say, pick, say_lang, say_seed, say_speed], [say_out, say_stats]
 
 
488
  )
489
 
490
- def on_language(language):
491
  choices = voices_in(language)
492
  name = choices[0][1]
493
- return (gr.Dropdown(choices=choices, value=name), *listen(name))
 
 
 
 
 
494
 
495
- lang_pick.change(on_language, lang_pick, [pick, made, ref, card])
496
- pick.change(listen, pick, [made, ref, card])
497
- demo.load(listen, pick, [made, ref, card])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
  with gr.Accordion("Determinism check", open=False):
500
  gr.Markdown(
501
- "This renders the same text twice at the same seed and hashes "
502
- "both waveforms. The digests must match."
 
503
  )
504
  with gr.Row():
505
  probe_text = gr.Textbox(
@@ -513,16 +629,18 @@ On-device text to speech, running here on ZeroGPU.
513
  probe_seed = gr.Number(value=7, precision=0, label="Seed", scale=1)
514
  probe_go = gr.Button("Render twice")
515
  probe_out = gr.Markdown()
516
- probe_go.click(probe, [probe_text, pick, probe_seed], probe_out)
517
 
518
  # ---------------------------- Clone ----------------------------
519
  with gr.Tab("Clone"):
520
  gr.Markdown(
521
  f"""
522
- Clone a voice from a short recording, then speak with it.
523
 
524
  - Try one of the shipped examples, or record yourself.
525
  - Clone only your own voice, or a voice you have permission to use.
 
 
526
  - Nothing is kept. The recording and the voice embeddings are discarded when the
527
  request ends, and neither is offered for download.
528
  - See [Responsible use]({RESPONSIBLE_USE}).
@@ -558,6 +676,9 @@ Clone a voice from a short recording, then speak with it.
558
  ),
559
  )
560
  with gr.Column(scale=1):
 
 
 
561
  clone_text = gr.Textbox(
562
  label="Text to speak",
563
  placeholder="Now in my own voice.",
@@ -596,7 +717,17 @@ Clone a voice from a short recording, then speak with it.
596
 
597
  clone_go.click(
598
  clone,
599
- [example, mic, upload, consent, clone_text, clone_lang, clone_seed, clone_speed],
 
 
 
 
 
 
 
 
 
 
600
  [clone_out, clone_stats],
601
  )
602
 
@@ -612,16 +743,22 @@ pip install "loudkit[torch,audio,enroll,hub]"
612
  ```python
613
  import loudkit as lk
614
 
615
- engine = lk.load("{REPO}")
616
- voice = lk.voice("kathleen", repo="{REPO}")
617
  engine.synthesize_long("Hello from loudkit.", voice, seed=7).save("hello.wav")
618
  ```
619
 
 
 
 
 
 
 
620
  Output files carry C2PA provenance: the fingerprint, the recipe and the seed.
621
  """
622
  )
623
 
624
- # The engine holds one set of weights and renders with an internal producer
625
  # thread. One render at a time keeps two requests off the same buffers.
626
  demo.queue(default_concurrency_limit=1, max_size=24)
627
 
 
1
+ """The loudkit demo Space: twenty-eight voices, two models, one engine.
2
 
3
  ZeroGPU bills GPU time to the *visitor*, not to the owner: an anonymous visitor
4
  gets about two minutes a day, a signed-in free account about five. A demo whose
5
  first click spends that budget is one most people bounce off before they have
6
+ heard anything at all. So the Voices tab is fifty-six pre-rendered files served
7
+ straight out of this repo, twenty-eight voices on each model, and the GPU is
8
+ spent only on what a visitor types or records.
9
+
10
+ Both models are here because the choice between them is the first real decision
11
+ an adopter makes, and it is not one a table of numbers settles. Every voice
12
+ reads the same passage on both, at the same seed, so switching the model is the
13
+ only thing that changes. loudr-1 is the natural one; loudr-1-turbo renders
14
+ about twice as fast from a two-token decode.
15
+
16
+ Both are loaded at module level on `cuda`, which is what ZeroGPU asks for: CUDA
17
+ transfers are optimised for start-up placement, and lazy-loading inside a
18
+ `@spaces.GPU` function is explicitly discouraged. Each decorated call then runs
19
+ in a freshly forked, short-lived process, which is also why there is no
20
+ `torch.compile` and no CUDA graph capture here: both pay their cost once per
21
+ process and would never amortise.
22
+
23
+ Cloning is exposed, and the consent is built into the shape of the tab rather
24
+ than written beside it: the microphone is the default path, an upload is
25
+ secondary and gated on an explicit confirmation, and neither recording outlives
26
+ the request that carried it.
27
+
28
+ Every load is pinned to a Hub revision. A demo that says what a build does must
29
+ be able to say which build, and `main` is not an answer that stays true.
30
  """
31
 
32
  from __future__ import annotations
 
51
  from loudkit.backends.torch_backend import build_torch_enroller
52
  from loudkit.hub import resolve_enrollment_checkpoint, resolve_voice_encoder
53
 
 
54
  DEVICE = "cuda"
55
  HERE = Path(__file__).parent
56
 
57
+ # The two published bundles, pinned. The tag moves when a release is cut; the
58
+ # commit does not, and a demo that claims a fingerprint should be reproducible
59
+ # from the same bytes a year from now.
60
+ MODELS = {
61
+ "loudr-1": {
62
+ "repo": "loudreader/loudr-1",
63
+ "revision": "7516673ee74ab2228ffc4e99223d296d8904d7ff",
64
+ "label": "loudr-1 · the natural one",
65
+ "blurb": "Single-token decode. The reference model.",
66
+ },
67
+ "turbo": {
68
+ "repo": "loudreader/loudr-1-turbo",
69
+ "revision": "366f140a5f7ebe215e5b43cdd73384746c83bae6",
70
+ "label": "loudr-1-turbo · about twice as fast",
71
+ "blurb": "Two-token decode, one flow step. Same voices, same profiles.",
72
+ },
73
+ }
74
+ DEFAULT_MODEL = "loudr-1"
75
+ MODEL_CHOICES = [(cfg["label"], key) for key, cfg in MODELS.items()]
76
+
77
  # The CPU scaffold capped text at 300 characters because CPU synthesis ran at
78
  # roughly a tenth of real time. On a GPU the cap is about the visitor's daily
79
  # quota instead, which is a far looser bound: 1000 characters is ~70 s of speech.
 
93
  ROSTER = json.loads((HERE / "voices.json").read_text(encoding="utf-8"))
94
  BY_NAME = {entry["name"]: entry for entry in ROSTER}
95
  ORDERED = sorted(ROSTER, key=lambda e: (e["language"], e["name"]))
 
96
 
97
  # English leads. A visitor who does not read the other nine should not have to
98
  # hunt for the one they do, and alphabetical order put Danish first.
99
  FIRST_LANGUAGE = "en"
100
  _LANG_OF = {e["language_id"]: e["language"] for e in ROSTER}
101
  LANGUAGE_FILTER = [(_LANG_OF[FIRST_LANGUAGE], FIRST_LANGUAGE)] + [
102
+ (name, code)
103
+ for code, name in sorted(_LANG_OF.items(), key=lambda kv: kv[1])
104
  if code != FIRST_LANGUAGE
105
  ]
106
 
 
128
  # Module-level model placement, per the ZeroGPU contract.
129
  # --------------------------------------------------------------------------
130
 
131
+ ENGINES = {
132
+ key: lk.load(cfg["repo"], revision=cfg["revision"], device=DEVICE)
133
+ for key, cfg in MODELS.items()
134
+ }
135
 
136
  # Voice profiles are numpy, not torch, so they are device-agnostic and cost a
137
+ # few hundred kilobytes each. They are also identical in both bundles, which is
138
+ # the point of a portable profile: one clone, either model.
139
+ BASE = MODELS[DEFAULT_MODEL]
140
+ PROFILES = {
141
+ entry["name"]: lk.voice(entry["name"], repo=BASE["repo"], revision=BASE["revision"])
142
+ for entry in ROSTER
143
+ }
144
 
145
  # Enrollment reads the other half of the release: the speech tokenizer and the
146
  # speaker encoder, which synthesis never touches, plus the utterance voice
147
+ # encoder that sits beside both. One enroller serves both models, because the
148
+ # profile it produces is the same file either way. `lk.enroll()` builds this per
149
+ # call by design; a Space would pay the load on every clone.
150
  enroller = build_torch_enroller(
151
+ str(resolve_enrollment_checkpoint(BASE["repo"], revision=BASE["revision"])),
152
  device=DEVICE,
153
+ voice_encoder_weights=str(resolve_voice_encoder(BASE["repo"], revision=BASE["revision"])),
154
  )
155
 
156
+ FINGERPRINTS = {key: engine.algorithm.fingerprint() for key, engine in ENGINES.items()}
157
 
158
  _LANGUAGE_NAMES = {e["language_id"]: e["language"] for e in ROSTER}
159
  LANGUAGE_CHOICES = [("Follow the voice", "")] + [
 
188
  return out.name
189
 
190
 
191
+ def _stats(result: lk.Result, model: str) -> str:
192
  seconds = len(result.audio) / result.sample_rate
193
  return (
194
  f"**{seconds:.1f} s of audio.** {result.timings.describe(seconds)}\n\n"
195
+ f"`{model}` · `algo[{result.algorithm_fingerprint}]` · seed `{result.seed}` · "
196
  f"speed `{result.speed:g}x` · {result.sample_rate} Hz"
197
  )
198
 
 
218
  return text
219
 
220
 
221
+ def _engine(model: str) -> lk.Engine:
222
+ return ENGINES[model if model in ENGINES else DEFAULT_MODEL]
223
+
224
+
225
  # --------------------------------------------------------------------------
226
  # Listen. No GPU: these files were rendered ahead of time and ship in the repo.
227
  # --------------------------------------------------------------------------
228
 
229
 
230
+ def listen(name: str, model: str):
231
  entry = BY_NAME[name]
232
  sample, reference, source = entry["sample"], entry["reference"], entry["source"]
233
+ model = model if model in MODELS else DEFAULT_MODEL
234
 
235
  lines = [
236
  f"### {entry['name']}. {entry['language']} ({entry['gender']}).",
237
  "",
238
  f"> {sample['text']}",
239
  "",
240
+ f"From *{sample['work']}*, seed `{sample['seed']}`, rendered on `{model}`.",
241
  "",
242
  f"- Reference recording: {reference['duration_s']:.1f} s, {reference['construction']}.",
243
  f"- Source: [{source['name']}]({source['url']}), {source['license']}.",
 
246
  similarity = entry.get("speaker_similarity")
247
  if similarity is not None:
248
  lines.append(f"- Speaker similarity to the reference: {similarity:.3f}.")
249
+ lines.append(f"- Voice profile: `{entry['profile']['hf_path']}`, the same file in both models.")
250
 
251
  return (
252
+ str(HERE / sample["audio"][model]),
253
  str(HERE / reference["public_preview"]),
254
  "\n".join(lines),
255
  )
 
272
  # --------------------------------------------------------------------------
273
 
274
 
275
+ def _speak_duration(text, name, model, language, seed, speed):
276
  return _estimate(text, overhead=15.0)
277
 
278
 
279
  @spaces.GPU(duration=_speak_duration)
280
+ def speak(text: str, name: str, model: str, language: str, seed: float, speed: float):
281
  text = _check(text, MAX_CHARS)
282
+ result = _engine(model).synthesize_long(
283
  text,
284
  PROFILES[name],
285
  seed=int(seed),
 
287
  speed=float(speed),
288
  )
289
  label = language or BY_NAME[name]["language_id"]
290
+ return _write(result, voice=name, language=label), _stats(result, model)
291
+
292
+
293
+ # --------------------------------------------------------------------------
294
+ # Both models on one text. GPU. The comparison the model choice is really about.
295
+ # --------------------------------------------------------------------------
296
+
297
+
298
+ def _compare_duration(text, name, seed):
299
+ return _estimate(text, passes=2, overhead=20.0)
300
+
301
+
302
+ @spaces.GPU(duration=_compare_duration)
303
+ def compare(text: str, name: str, seed: float):
304
+ """The same words, the same voice, the same seed, on both models."""
305
+ text = _check(text, MAX_PROBE_CHARS)
306
+ profile = PROFILES[name]
307
+ out = {}
308
+ for key, engine in ENGINES.items():
309
+ result = engine.synthesize_long(text, profile, seed=int(seed))
310
+ out[key] = (
311
+ _write(result, voice=name, language=BY_NAME[name]["language_id"]),
312
+ len(result.audio) / result.sample_rate,
313
+ result.algorithm_fingerprint,
314
+ )
315
+
316
+ note = "\n".join(
317
+ [
318
+ "| model | audio | fingerprint |",
319
+ "|---|---:|---|",
320
+ *(
321
+ f"| `{key}` | {seconds:.2f} s | `{fingerprint}` |"
322
+ for key, (_, seconds, fingerprint) in out.items()
323
+ ),
324
+ "",
325
+ "Two models, two fingerprints, and the same voice profile in both. "
326
+ "Which one to ship is an ear question, not a table question.",
327
+ ]
328
+ )
329
+ return out["loudr-1"][0], out["turbo"][0], note
330
 
331
 
332
  # --------------------------------------------------------------------------
 
334
  # --------------------------------------------------------------------------
335
 
336
 
337
+ def _clone_duration(example, mic, upload, consent, text, model, language, seed, speed):
338
  # Enrollment is a fixed cost on top of the render: two encoders and a
339
  # tokenizer over at most 20 s of audio.
340
  return _estimate(text, overhead=30.0)
341
 
342
 
343
  @spaces.GPU(duration=_clone_duration)
344
+ def clone(example, mic, upload, consent: bool, text: str, model: str, language: str, seed, speed):
345
  """Enroll a voice, speak with it, keep nothing.
346
 
347
  Three ways in, and they do not carry the same consent story, so they are
 
394
  # and read its text through the English funnel.
395
  profile = dataclasses.replace(profile, language=language or "en")
396
 
397
+ result = _engine(model).synthesize_long(
398
  text, profile, seed=int(seed), language=language or None, speed=float(speed)
399
  )
400
+ return _write(result, voice=label, language=profile.language), _stats(result, model)
401
  finally:
402
  # Nothing the visitor recorded outlives the request that carried it.
403
  # A shipped example is not the visitor's and is not ours to delete.
 
411
  # --------------------------------------------------------------------------
412
 
413
 
414
+ def _probe_duration(text, name, model, seed):
415
  return _estimate(text, passes=2, overhead=20.0)
416
 
417
 
418
  @spaces.GPU(duration=_probe_duration)
419
+ def probe(text: str, name: str, model: str, seed: float):
420
  text = _check(text, MAX_PROBE_CHARS)
421
+ engine = _engine(model)
422
  profile = PROFILES[name]
423
  first = engine.synthesize_long(text, profile, seed=int(seed))
424
  second = engine.synthesize_long(text, profile, seed=int(seed))
 
433
  "```",
434
  f"render 1 sha256 {left}",
435
  f"render 2 sha256 {right}",
436
+ f" algo[{first.algorithm_fingerprint}] seed {int(seed)} {model}",
437
  "```",
438
  "",
439
  "Identical within this build and this device. loudkit promises a "
440
  "bit-identical waveform for the same seed, build, backend and input. "
441
+ "It does not promise that your laptop matches this GPU, and the two "
442
+ "models are two different builds by construction. "
443
  f"[Read the identity contract]({IDENTITY_CONTRACT}).",
444
  ]
445
  )
 
497
  with gr.Blocks(title="loudkit", theme=THEME, css=CSS, js=FORCE_LIGHT, fill_width=False) as demo:
498
  gr.Markdown(
499
  f"""
500
+ # Twenty-eight voices. Ten languages. Two models.
501
 
502
  On-device text to speech, running here on ZeroGPU.
503
+ [loudr-1](https://huggingface.co/{MODELS["loudr-1"]["repo"]}) ·
504
+ [loudr-1-turbo](https://huggingface.co/{MODELS["turbo"]["repo"]}) ·
505
+ [Code]({DOCS}) · [Responsible use]({RESPONSIBLE_USE})
506
 
507
  <span class="lk-pill">Listening costs no GPU</span>
508
  <span class="lk-pill">Speaking and cloning spend your daily quota</span>
509
+ <span class="lk-pill">algo[{FINGERPRINTS["loudr-1"]}]</span>
510
+ <span class="lk-pill">algo[{FINGERPRINTS["turbo"]}]</span>
511
  """,
512
  elem_id="lk-head",
513
  )
 
516
  # ------------- Voices: listen for free, then type your own -------------
517
  with gr.Tab("Voices"):
518
  gr.Markdown(
519
+ "Pick a voice and the sample plays at once. Every voice is here "
520
+ "twice, once per model, reading the same passage at the same "
521
+ "seed. Those files were rendered ahead of time and use no GPU."
522
  )
523
  with gr.Row():
524
  with gr.Column(scale=1):
525
+ model_pick = gr.Radio(
526
+ MODEL_CHOICES,
527
+ value=DEFAULT_MODEL,
528
+ label="Model",
529
+ info="Switching this changes the sample and what the buttons below run.",
530
+ )
531
  with gr.Row():
532
  lang_pick = gr.Dropdown(
533
  LANGUAGE_FILTER, value=FIRST_LANGUAGE, label="Language"
534
  )
535
+ pick = gr.Dropdown(FIRST_VOICES, value=FIRST_VOICE, label="Voice")
 
 
536
  made = gr.Audio(label="loudkit", type="filepath", interactive=False)
537
  ref = gr.Audio(
538
  label="Reference recording", type="filepath", interactive=False
 
573
  say_stats = gr.Markdown()
574
 
575
  say_go.click(
576
+ speak,
577
+ [say, pick, model_pick, say_lang, say_seed, say_speed],
578
+ [say_out, say_stats],
579
  )
580
 
581
+ def on_language(language, model):
582
  choices = voices_in(language)
583
  name = choices[0][1]
584
+ return (gr.Dropdown(choices=choices, value=name), *listen(name, model))
585
+
586
+ lang_pick.change(on_language, [lang_pick, model_pick], [pick, made, ref, card])
587
+ pick.change(listen, [pick, model_pick], [made, ref, card])
588
+ model_pick.change(listen, [pick, model_pick], [made, ref, card])
589
+ demo.load(listen, [pick, model_pick], [made, ref, card])
590
 
591
+ with gr.Accordion("Hear both models on your own words", open=False):
592
+ gr.Markdown(
593
+ "The same text, the same voice, the same seed, rendered on "
594
+ "both. This is the comparison the model choice is about, and "
595
+ "it costs two renders of your quota."
596
+ )
597
+ with gr.Row():
598
+ cmp_text = gr.Textbox(
599
+ value="The same words, on both models.",
600
+ label="Text",
601
+ lines=1,
602
+ max_lines=2,
603
+ max_length=MAX_PROBE_CHARS,
604
+ scale=3,
605
+ )
606
+ cmp_seed = gr.Number(value=7, precision=0, label="Seed", scale=1)
607
+ cmp_go = gr.Button("Render on both")
608
+ with gr.Row():
609
+ cmp_base = gr.Audio(label="loudr-1", type="filepath")
610
+ cmp_turbo = gr.Audio(label="loudr-1-turbo", type="filepath")
611
+ cmp_note = gr.Markdown()
612
+ cmp_go.click(compare, [cmp_text, pick, cmp_seed], [cmp_base, cmp_turbo, cmp_note])
613
 
614
  with gr.Accordion("Determinism check", open=False):
615
  gr.Markdown(
616
+ "This renders the same text twice at the same seed on the "
617
+ "selected model and hashes both waveforms. The digests must "
618
+ "match."
619
  )
620
  with gr.Row():
621
  probe_text = gr.Textbox(
 
629
  probe_seed = gr.Number(value=7, precision=0, label="Seed", scale=1)
630
  probe_go = gr.Button("Render twice")
631
  probe_out = gr.Markdown()
632
+ probe_go.click(probe, [probe_text, pick, model_pick, probe_seed], probe_out)
633
 
634
  # ---------------------------- Clone ----------------------------
635
  with gr.Tab("Clone"):
636
  gr.Markdown(
637
  f"""
638
+ Clone a voice from a short recording, then speak with it on either model.
639
 
640
  - Try one of the shipped examples, or record yourself.
641
  - Clone only your own voice, or a voice you have permission to use.
642
+ - One profile serves both models. That is what a portable voice profile means:
643
+ enroll once, and the file works wherever the engine does.
644
  - Nothing is kept. The recording and the voice embeddings are discarded when the
645
  request ends, and neither is offered for download.
646
  - See [Responsible use]({RESPONSIBLE_USE}).
 
676
  ),
677
  )
678
  with gr.Column(scale=1):
679
+ clone_model = gr.Radio(
680
+ MODEL_CHOICES, value=DEFAULT_MODEL, label="Speak it with"
681
+ )
682
  clone_text = gr.Textbox(
683
  label="Text to speak",
684
  placeholder="Now in my own voice.",
 
717
 
718
  clone_go.click(
719
  clone,
720
+ [
721
+ example,
722
+ mic,
723
+ upload,
724
+ consent,
725
+ clone_text,
726
+ clone_model,
727
+ clone_lang,
728
+ clone_seed,
729
+ clone_speed,
730
+ ],
731
  [clone_out, clone_stats],
732
  )
733
 
 
743
  ```python
744
  import loudkit as lk
745
 
746
+ engine = lk.load("{MODELS["loudr-1"]["repo"]}", revision="v0.1.1")
747
+ voice = lk.voice("kathleen", repo="{MODELS["loudr-1"]["repo"]}", revision="v0.1.1")
748
  engine.synthesize_long("Hello from loudkit.", voice, seed=7).save("hello.wav")
749
  ```
750
 
751
+ The same profile reads on the faster model by changing one string:
752
+
753
+ ```python
754
+ engine = lk.load("{MODELS["turbo"]["repo"]}", revision="v0.1.1")
755
+ ```
756
+
757
  Output files carry C2PA provenance: the fingerprint, the recipe and the seed.
758
  """
759
  )
760
 
761
+ # Each engine holds one set of weights and renders with an internal producer
762
  # thread. One render at a time keeps two requests off the same buffers.
763
  demo.queue(default_concurrency_limit=1, max_size=24)
764
 
audio/carmen.opus DELETED
Binary file (40.8 kB)
 
audio/colette.opus DELETED
Binary file (69.7 kB)
 
audio/dante.opus DELETED
Binary file (44.7 kB)
 
audio/darkman.opus DELETED
Binary file (32.9 kB)
 
audio/dave.opus DELETED
Binary file (36.3 kB)
 
audio/freja.opus DELETED
Binary file (36.3 kB)
 
audio/gosia.opus DELETED
Binary file (31.3 kB)
 
audio/henri.opus DELETED
Binary file (62.7 kB)
 
audio/ines.opus DELETED
Binary file (57 kB)
 
audio/joe.opus DELETED
Binary file (52.2 kB)
 
audio/kathleen.opus DELETED
Binary file (55.8 kB)
 
audio/kerstin.opus DELETED
Binary file (48.9 kB)
 
audio/loudr-1/carmen.opus ADDED
Binary file (62.8 kB). View file
 
audio/loudr-1/clara.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d218b620146d5954adefb4dbf631c169b4647967bf49b7383e03997468a636e3
3
+ size 132921
audio/loudr-1/colette.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:04a10c727da4572bb53cf7919fa3404c3a46376a7603fe370ddcd1d62dfb977a
3
+ size 106160
audio/loudr-1/dante.opus ADDED
Binary file (72.6 kB). View file
 
audio/loudr-1/darkman.opus ADDED
Binary file (50.5 kB). View file
 
audio/loudr-1/dave.opus ADDED
Binary file (58.6 kB). View file
 
audio/loudr-1/emma.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a89a1dda44d632f9a32bf08db296e3a17cafb28f73aa7b354f7882c502dd9340
3
+ size 125680
audio/loudr-1/freja.opus ADDED
Binary file (62.3 kB). View file
 
audio/loudr-1/gosia.opus ADDED
Binary file (45 kB). View file
 
audio/loudr-1/henri.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3fd1de078ca5bffece9b03b6cc11100a6ea88125ab90ca64337ce0e5ce08fac2
3
+ size 102335
audio/loudr-1/henry.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:816ac99dcadf1b6132675b8e8f9444f19346c13e271bb42c0298735835c7c691
3
+ size 159500
audio/loudr-1/ines.opus ADDED
Binary file (81.8 kB). View file
 
audio/loudr-1/joe.opus ADDED
Binary file (84.3 kB). View file
 
audio/loudr-1/kathleen.opus ADDED
Binary file (79 kB). View file
 
audio/loudr-1/kerstin.opus ADDED
Binary file (83.4 kB). View file
 
audio/loudr-1/lucy.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34d32240a95c4765c0e38b12a031f0d054be0d1c29de6739069454523d2b6a5e
3
+ size 124298
audio/loudr-1/miles.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81734391dc7570f8b632244ee948b2736b7f02b3449ca88522caa5304a1973f0
3
+ size 166129
audio/loudr-1/nathalie.opus ADDED
Binary file (79.2 kB). View file
 
audio/loudr-1/nils.opus ADDED
Binary file (61.2 kB). View file
 
audio/loudr-1/oliver.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:920cb343127a0bca928c1b67a846dd431f9e63b696210dd693007f8efcaa8319
3
+ size 141153
audio/loudr-1/oscar.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e696f00b74d4fe3cc1d89f95d40c6999756f33269b002c50f181c6658667257c
3
+ size 140099
audio/loudr-1/paola.opus ADDED
Binary file (71.5 kB). View file
 
audio/loudr-1/pim.opus ADDED
Binary file (75.5 kB). View file
 
audio/loudr-1/selma.opus ADDED
Binary file (42.3 kB). View file
 
audio/loudr-1/sophie.opus ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b44b159cc733894d77a14c97105442092c65d42bb2e2422927af2e631704d149
3
+ size 147289
audio/loudr-1/soren.opus ADDED
Binary file (57.7 kB). View file
 
audio/loudr-1/thorsten.opus ADDED
Binary file (81.6 kB). View file
 
audio/loudr-1/tugao.opus ADDED
Binary file (79.9 kB). View file
 
audio/nathalie.opus DELETED
Binary file (42.3 kB)
 
audio/nils.opus DELETED
Binary file (36.7 kB)
 
audio/paola.opus DELETED
Binary file (50.2 kB)
 
audio/pim.opus DELETED
Binary file (42.2 kB)
 
audio/refs/clara.opus ADDED
Binary file (78.1 kB). View file
 
audio/refs/emma.opus ADDED
Binary file (80 kB). View file
 
audio/refs/henry.opus ADDED
Binary file (76.3 kB). View file