|
311 | 311 | "source": [ |
312 | 312 | "### Model selection\n", |
313 | 313 | "\n", |
314 | | - "The separation models trained on wsj0_2mix are published in the [ESPnet\n", |
315 | | - "organisation](https://huggingface.co/espnet) and load by tag. The course used\n", |
316 | | - "a TF-GridNet checkpoint kept on Google Drive; the Hub copy of that one is an\n", |
317 | | - "empty repository, so this uses DPTNet instead, which is published properly and\n", |
318 | | - "separates the same mixtures.\n" |
| 314 | + "Separation is a different task from the enhancement above: one microphone, two\n", |
| 315 | + "people talking at once, and one output per speaker rather than one clean\n", |
| 316 | + "output.\n", |
| 317 | + "\n", |
| 318 | + "The model is\n", |
| 319 | + "[`espnet/anogkongda_librimix_enh_train_raw_valid.si_snr.ave`](https://huggingface.co/espnet/anogkongda_librimix_enh_train_raw_valid.si_snr.ave),\n", |
| 320 | + "a Conv-TasNet trained on Libri2Mix at 8 kHz and published in the ESPnet\n", |
| 321 | + "organisation. The course used a TF-GridNet checkpoint kept on Google Drive,\n", |
| 322 | + "and a wsj0-2mix mixture from the same place; wsj0-2mix is built on WSJ0 and\n", |
| 323 | + "cannot be published, while Libri2Mix is built on LibriSpeech, which is CC BY\n", |
| 324 | + "4.0 — so this notebook can fetch both the model and the audio it separates." |
319 | 325 | ] |
320 | 326 | }, |
321 | 327 | { |
|
327 | 333 | "from espnet2.bin.enh_inference import SeparateSpeech\n", |
328 | 334 | "\n", |
329 | 335 | "separate_speech = SeparateSpeech.from_pretrained(\n", |
330 | | - " model_tag=\"espnet/Wangyou_Zhang_wsj0_2mix_enh_train_enh_dptnet_raw\",\n", |
331 | | - " # for segment-wise process on long speech\n", |
332 | | - " segment_size=2.4,\n", |
333 | | - " hop_size=0.8,\n", |
334 | | - " normalize_segment_scale=False,\n", |
335 | | - " show_progressbar=True,\n", |
336 | | - " ref_channel=None,\n", |
| 336 | + " model_tag=\"espnet/anogkongda_librimix_enh_train_raw_valid.si_snr.ave\",\n", |
337 | 337 | " normalize_output_wav=True,\n", |
338 | 338 | " device=\"cuda\" if torch.cuda.is_available() else \"cpu\",\n", |
339 | | - ")\n" |
| 339 | + ")" |
340 | 340 | ], |
341 | 341 | "execution_count": null, |
342 | 342 | "outputs": [] |
|
352 | 352 | "cell_type": "markdown", |
353 | 353 | "metadata": {}, |
354 | 354 | "source": [ |
355 | | - "#### Separate the example in wsj0_2mix testing set\n" |
| 355 | + "#### Separate one mixture from the Libri2Mix test set" |
356 | 356 | ] |
357 | 357 | }, |
358 | 358 | { |
359 | 359 | "cell_type": "markdown", |
360 | 360 | "source": [ |
361 | | - "### Task2 )\n", |
| 361 | + "The mixture is built here rather than downloaded, the way LibriMix builds its\n", |
| 362 | + "own: take the two gains the official metadata gives for this pair, resample to\n", |
| 363 | + "the model's 8 kHz, truncate to the shorter source — LibriMix's `min` mode —\n", |
| 364 | + "and add them together.\n", |
362 | 365 | "\n", |
363 | | - "Run inference of pretrained speech seperation model based on TF-GRIDNET.\n" |
| 366 | + "Building it from the two sources is worth the six lines: it means the\n", |
| 367 | + "references are in hand, so the separation can be **scored** further down\n", |
| 368 | + "rather than only listened to." |
364 | 369 | ], |
365 | 370 | "metadata": {} |
366 | 371 | }, |
367 | 372 | { |
368 | 373 | "cell_type": "code", |
369 | 374 | "metadata": {}, |
370 | 375 | "source": [ |
371 | | - "# Two people talking at once. The model was trained on wsj0-2mix, whose\n", |
372 | | - "# mixtures are built from WSJ0 and so cannot be published; this makes one the\n", |
373 | | - "# same way - two recordings of different speakers, summed at equal level.\n", |
374 | | - "!wget -q -O spk1.wav https://github.com/espnet/espnet/raw/master/test_utils/ctc_align_test.wav\n", |
375 | | - "!wget -q -O spk2.wav https://github.com/espnet/espnet/raw/master/test_utils/st_test.wav\n", |
| 376 | + "# Mixture 7729-102255-0031_2094-142345-0028 of the Libri2Mix test set, named in\n", |
| 377 | + "# metadata/Libri2Mix/libri2mix_test-clean.csv of the LibriMix repository. The\n", |
| 378 | + "# two sources are LibriSpeech test-clean utterances (CC BY 4.0), kept beside\n", |
| 379 | + "# the model so that this notebook does not need the corpus.\n", |
| 380 | + "!wget -q -O spk1.flac https://huggingface.co/espnet/anogkongda_librimix_enh_train_raw_valid.si_snr.ave/resolve/main/samples/7729-102255-0031.flac\n", |
| 381 | + "!wget -q -O spk2.flac https://huggingface.co/espnet/anogkongda_librimix_enh_train_raw_valid.si_snr.ave/resolve/main/samples/2094-142345-0028.flac\n", |
376 | 382 | "\n", |
377 | | - "import os\n", |
378 | 383 | "import numpy as np\n", |
379 | 384 | "import soundfile\n", |
380 | 385 | "from IPython.display import display, Audio\n", |
| 386 | + "from scipy.signal import resample_poly\n", |
381 | 387 | "\n", |
382 | | - "first, sr = soundfile.read(\"spk1.wav\")\n", |
383 | | - "second, _ = soundfile.read(\"spk2.wav\")\n", |
384 | | - "length = min(len(first), len(second))\n", |
385 | | - "mixwav = first[:length] + second[:length]\n", |
386 | | - "mixwav = mixwav / np.abs(mixwav).max() * 0.9\n", |
387 | | - "soundfile.write(\"two_speakers.wav\", mixwav, sr)\n", |
388 | | - "waves_wsj = separate_speech(mixwav[None, ...], fs=sr)\n", |
| 388 | + "# the gains that row of the metadata gives, and the model's sampling rate\n", |
| 389 | + "GAINS = {\"spk1.flac\": 0.5159350719675272, \"spk2.flac\": 0.22360240150798563}\n", |
| 390 | + "FS = 8000\n", |
| 391 | + "\n", |
| 392 | + "refs = []\n", |
| 393 | + "for name, gain in GAINS.items():\n", |
| 394 | + " source, fs = soundfile.read(name, dtype=\"float32\")\n", |
| 395 | + " refs.append(resample_poly(source * gain, FS, fs))\n", |
| 396 | + "length = min(len(ref) for ref in refs) # LibriMix's \"min\" mode\n", |
| 397 | + "refs = [ref[:length] for ref in refs]\n", |
| 398 | + "mixwav = sum(refs)\n", |
| 399 | + "sr = FS\n", |
| 400 | + "\n", |
| 401 | + "waves_librimix = separate_speech(mixwav[None, ...], fs=sr)\n", |
389 | 402 | "\n", |
390 | 403 | "print(\"Input mixture\", flush=True)\n", |
391 | 404 | "display(Audio(mixwav, rate=sr))\n", |
392 | | - "print(f\"========= Separated speech with model =========\", flush=True)\n", |
393 | | - "print(\"Separated spk1\", flush=True)\n", |
394 | | - "display(Audio(waves_wsj[0].squeeze(), rate=sr))\n", |
395 | | - "print(\"Separated spk2\", flush=True)\n", |
396 | | - "display(Audio(waves_wsj[1].squeeze(), rate=sr))" |
| 405 | + "for i, wave in enumerate(waves_librimix, start=1):\n", |
| 406 | + " print(f\"========= Separated speaker {i} =========\", flush=True)\n", |
| 407 | + " display(Audio(wave.squeeze(), rate=sr))" |
| 408 | + ], |
| 409 | + "execution_count": null, |
| 410 | + "outputs": [] |
| 411 | + }, |
| 412 | + { |
| 413 | + "cell_type": "markdown", |
| 414 | + "metadata": {}, |
| 415 | + "source": [ |
| 416 | + "#### How much of each speaker came back\n", |
| 417 | + "\n", |
| 418 | + "Both sources are in hand, so this is measurable. SI-SNR asks how much of the\n", |
| 419 | + "signal is the speaker you wanted and how much is everything else, after the\n", |
| 420 | + "best scaling — one number per speaker, in dB, for the mixture going in and for\n", |
| 421 | + "the separated stream coming out." |
| 422 | + ] |
| 423 | + }, |
| 424 | + { |
| 425 | + "cell_type": "code", |
| 426 | + "metadata": {}, |
| 427 | + "source": [ |
| 428 | + "from itertools import permutations\n", |
| 429 | + "\n", |
| 430 | + "\n", |
| 431 | + "def si_snr(estimate, reference):\n", |
| 432 | + " \"\"\"Scale-invariant signal-to-noise ratio, in dB.\"\"\"\n", |
| 433 | + " estimate = estimate - estimate.mean()\n", |
| 434 | + " reference = reference - reference.mean()\n", |
| 435 | + " scale = np.dot(estimate, reference) / np.dot(reference, reference)\n", |
| 436 | + " target = scale * reference\n", |
| 437 | + " noise = estimate - target\n", |
| 438 | + " return 10 * np.log10(np.dot(target, target) / np.dot(noise, noise))\n", |
| 439 | + "\n", |
| 440 | + "\n", |
| 441 | + "estimates = [wave.squeeze() for wave in waves_librimix]\n", |
| 442 | + "# separation has no speaker order, so the scoring picks the better pairing\n", |
| 443 | + "order = max(\n", |
| 444 | + " permutations(range(len(estimates))),\n", |
| 445 | + " key=lambda p: sum(si_snr(estimates[p[i]], refs[i]) for i in range(len(refs))),\n", |
| 446 | + ")\n", |
| 447 | + "estimates = [estimates[i] for i in order]\n", |
| 448 | + "\n", |
| 449 | + "for i, (estimate, reference) in enumerate(zip(estimates, refs), start=1):\n", |
| 450 | + " before = si_snr(mixwav[: len(reference)], reference)\n", |
| 451 | + " after = si_snr(estimate[: len(reference)], reference)\n", |
| 452 | + " print(f\"speaker {i}: {before:6.2f} dB in the mixture -> {after:6.2f} dB separated\")" |
397 | 453 | ], |
398 | 454 | "execution_count": null, |
399 | 455 | "outputs": [] |
|
454 | 510 | "\n", |
455 | 511 | "fig, axes = plt.subplots(3, 2, figsize=(18, 9))\n", |
456 | 512 | "for row, (wav, name) in enumerate(\n", |
457 | | - " [(mixwav, \"Mixture\"), (waves_wsj[0], \"Separated (spk1)\"),\n", |
458 | | - " (waves_wsj[1], \"Separated (spk2)\")]\n", |
| 513 | + " [(mixwav, \"Mixture\"), (waves_librimix[0], \"Separated (spk1)\"),\n", |
| 514 | + " (waves_librimix[1], \"Separated (spk2)\")]\n", |
459 | 515 | "):\n", |
460 | 516 | " plot_spectrogram(axes[row][0], wav, f\"{name} spectrogram\")\n", |
461 | 517 | " plot_waveform(axes[row][1], wav, f\"{name} waveform\")\n", |
462 | 518 | "axes[2][1].set_xlabel(\"Time (s)\")\n", |
463 | 519 | "plt.tight_layout()\n", |
464 | | - "plt.show()\n" |
| 520 | + "plt.show()\n", |
| 521 | + "" |
465 | 522 | ], |
466 | 523 | "execution_count": null, |
467 | 524 | "outputs": [] |
|
470 | 527 | "cell_type": "markdown", |
471 | 528 | "metadata": {}, |
472 | 529 | "source": [ |
473 | | - "## Evaluate separated speech with pretrained ASR model\n", |
| 530 | + "## Evaluate separated speech with a pretrained ASR model\n", |
474 | 531 | "\n", |
475 | | - "The ground truths are:\n", |
| 532 | + "SI-SNR says how much of each speaker came back. This asks the question the\n", |
| 533 | + "other way round: can something else use the result? The transcripts of the two\n", |
| 534 | + "sources are\n", |
476 | 535 | "\n", |
477 | | - "`text_1: SOME CRITICS INCLUDING HIGH REAGAN ADMINISTRATION OFFICIALS ARE RAISING THE ALARM THAT THE FED'S POLICY IS TOO TIGHT AND COULD CAUSE A RECESSION NEXT YEAR`\n", |
| 536 | + "`text_1: HE CONTINUED HIS PRETENDED SEARCH AND TO GIVE COLOR TO HIS ERRAND MADE TWO ARRESTS`\n", |
478 | 537 | "\n", |
479 | | - "`text_2: THE UNITED STATES UNDERTOOK TO DEFEND WESTERN EUROPE AGAINST SOVIET ATTACK`\n", |
| 538 | + "`text_2: NO NO NO TOTTY UD GET HER FEET WET SAID MISSUS POYSER CARRYING AWAY HER IRON`\n", |
480 | 539 | "\n", |
481 | | - "(This may take a while for the speech recognition.)\n" |
| 540 | + "(This may take a while for the speech recognition.)" |
482 | 541 | ] |
483 | 542 | }, |
484 | 543 | { |
|
493 | 552 | { |
494 | 553 | "cell_type": "markdown", |
495 | 554 | "source": [ |
496 | | - "### Task3 )\n", |
| 555 | + "### Recognise the mixture, and the separated streams\n", |
497 | 556 | "\n", |
498 | | - "Show inference of pre-trained ASR model (trained on WSJ corpus) on mixed and seperated speech.\n" |
| 557 | + "The model is a transformer trained on LibriSpeech, which is where both of\n", |
| 558 | + "these recordings come from. It runs at 16 kHz while separation ran at 8 kHz,\n", |
| 559 | + "so each waveform is resampled on the way in — the interesting comparison is\n", |
| 560 | + "the mixture against the separated streams, and that is unaffected." |
499 | 561 | ], |
500 | 562 | "metadata": {} |
501 | 563 | }, |
|
506 | 568 | "import torch\n", |
507 | 569 | "from espnet2.bin.asr_inference import Speech2Text\n", |
508 | 570 | "\n", |
509 | | - "# a WSJ model from the organisation, so nothing here depends on a Google\n", |
510 | | - "# Drive copy staying up\n", |
511 | 571 | "speech2text = Speech2Text.from_pretrained(\n", |
512 | | - " model_tag=\"espnet/kamo-naoyuki_wsj_transformer2\",\n", |
| 572 | + " model_tag=(\n", |
| 573 | + " \"espnet/Shinji_Watanabe_librispeech_asr_train_asr_transformer_e18\"\n", |
| 574 | + " \"_raw_bpe_sp_valid.acc.best\"\n", |
| 575 | + " ),\n", |
513 | 576 | " ngram_weight=0.0,\n", |
514 | 577 | " lm_weight=0.0,\n", |
515 | 578 | " device=\"cuda\" if torch.cuda.is_available() else \"cpu\",\n", |
516 | 579 | ")\n", |
517 | 580 | "\n", |
518 | | - "text_est = [None, None]\n", |
519 | | - "text_est[0], *_ = speech2text(waves_wsj[0].squeeze())[0]\n", |
520 | | - "text_est[1], *_ = speech2text(waves_wsj[1].squeeze())[0]\n", |
521 | | - "text_m, *_ = speech2text(mixwav)[0]\n", |
| 581 | + "ASR_FS = 16000\n", |
| 582 | + "\n", |
| 583 | + "\n", |
| 584 | + "def recognise(wav):\n", |
| 585 | + " \"\"\"Transcribe one 8 kHz waveform with a 16 kHz model.\"\"\"\n", |
| 586 | + " text, *_ = speech2text(resample_poly(wav, ASR_FS, sr).astype(\"float32\"))[0]\n", |
| 587 | + " return text\n", |
| 588 | + "\n", |
| 589 | + "\n", |
| 590 | + "text_est = [recognise(estimate) for estimate in estimates]\n", |
| 591 | + "text_m = recognise(mixwav)\n", |
522 | 592 | "print(\"Mix Speech to Text: \", text_m)\n", |
523 | 593 | "print(\"Separated Speech 1 to Text: \", text_est[0])\n", |
524 | | - "print(\"Separated Speech 2 to Text: \", text_est[1])\n" |
| 594 | + "print(\"Separated Speech 2 to Text: \", text_est[1])" |
525 | 595 | ], |
526 | 596 | "execution_count": null, |
527 | 597 | "outputs": [] |
|
580 | 650 | "\n", |
581 | 651 | "\n", |
582 | 652 | "text_ref = [\n", |
583 | | - " \"SOME CRITICS INCLUDING HIGH REAGAN ADMINISTRATION OFFICIALS ARE RAISING THE ALARM THAT THE FED'S POLICY IS TOO TIGHT AND COULD CAUSE A RECESSION NEXT YEAR\",\n", |
584 | | - " \"THE UNITED STATES UNDERTOOK TO DEFEND WESTERN EUROPE AGAINST SOVIET ATTACK\",\n", |
| 653 | + " \"HE CONTINUED HIS PRETENDED SEARCH AND TO GIVE COLOR TO HIS ERRAND MADE TWO ARRESTS\",\n", |
| 654 | + " \"NO NO NO TOTTY UD GET HER FEET WET SAID MISSUS POYSER CARRYING AWAY HER IRON\",\n", |
585 | 655 | "]\n", |
586 | 656 | "\n", |
587 | 657 | "print(\"=====================\" , flush=True)\n", |
|
0 commit comments