◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, which is tomorrow. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, which is tomorrow. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-09-07Intermediate

The day Lyria 3.5 landed, I changed how my audio folders are laid out

When Lyria 3.5 brought full-length generation, my generated takes were sitting in the same folder as the tracks I had chosen by hand. Here is the forty-line ledger gate that draws the line by hash, not by filename.

Lyria 3.5music generationGemini API250Python52indie development28release management

I read that Lyria 3.5 had moved into public preview on September 3rd while I was in the middle of reshuffling candidate tracks for the next update of my healing-sound app. Full-length music generation, finer control over length and structure, 44.1kHz stereo output — I stopped reading there.

What stopped me was closer to unease than excitement. Looking at the folder in front of me, the tracks I had chosen by hand and the takes I had generated for comparison were sitting at the same level. I had prefixed the generated ones with draft_, but that prefix was the whole of my boundary.

Let me say up front that this is not a piece about how well the model performs. The better generation gets, the more the decision about what belongs in the shipped product has to be protected by a mechanism rather than by mood. I ended up putting that mechanism into a forty-odd-line script.

A filename prefix disappears on the first copy

For a while I assumed the prefix was enough. Generated takes got draft_, hand-picked tracks kept their names, and I could tell them apart by eye.

That did not hold up. Run the files through an export tool, move them to another machine, renumber a batch — the prefix comes off each time, and the only moment I could notice was the next time I happened to open that file.

Sure enough, a file called calm_13_new.wav was still sitting in the candidate folder. Looking back, it was something I had set aside meaning to decide later, and then forgot. From the name alone I could not tell whether I had chosen it myself or dropped it in as a generated comparison.

Filenames, locations, and modification times all get rewritten by a single human action. Unless the primary key is something that does not get rewritten, the boundary will not hold.

What Lyria 3.5 changed (public preview, September 3, 2026)

Here are the facts first.

ItemDetail
AvailabilityPublic preview (September 3, 2026)
LengthFull-length music generation
ImprovementsMusical coherence, natural vocals, finer control over length and structure
InputText and images
OutputHigh-fidelity 44.1kHz stereo audio

The line that mattered to me was not about fidelity. It was that a whole piece now arrives in one go. A 30-second clip announces itself as a sketch the moment you hear it. Several minutes of finished-sounding music sits in the folder looking exactly like a candidate.

You cannot keep sorting things by eye once they stop looking different. That is the point where I decided to change how the folders were laid out.

Only the hash of the contents survives a copy

So I moved the primary key of my ledger from the filename to the SHA-256 of the contents. Rename the file, move the folder, touch the timestamp — as long as the bytes are the same, the hash is the same. And if I re-export with different settings, the hash changes too, which means "I rebuilt it and forgot to register it" gets caught by the same mechanism.

The ledger is one line per track, tab-separated, with three columns.

ColumnValueMeaning
1SHA-256Fingerprint of the contents. This is the key
2originhand (chosen by me) / generated / reference
3noteA human-readable memo, usually the original filename

And only hand is allowed in the ship folder.

Tracks I chose by hand go out; generated takes stay on the scouting shelf. Writing that single rule into a constant in the script, rather than keeping it in my head, is what made the hesitation go away.

The forty-odd-line ledger gate

Here is the script as it stands. Standard library only.

#!/usr/bin/env python3
"""Gate that lets only approved audio into the ship folder."""
import hashlib
import pathlib
import sys
 
AUDIO_SUFFIXES = {".wav", ".mp3", ".m4a", ".aac", ".flac", ".ogg", ".caf"}
SHIPPABLE = {"hand"}
 
 
def digest(path: pathlib.Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()
 
 
def load_ledger(path: pathlib.Path) -> dict:
    ledger = {}
    for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        parts = line.split("\t")
        if len(parts) < 3:
            sys.exit(f"ledger line {lineno} has too few columns: {raw!r}")
        ledger[parts[0]] = (parts[1], parts[2])
    return ledger
 
 
def main(ship_dir: str, ledger_file: str) -> int:
    ledger = load_ledger(pathlib.Path(ledger_file))
    unregistered, blocked, passed = [], [], 0
    for path in sorted(pathlib.Path(ship_dir).rglob("*")):
        if not path.is_file() or path.suffix.lower() not in AUDIO_SUFFIXES:
            continue
        entry = ledger.get(digest(path))
        if entry is None:
            unregistered.append(path)
        elif entry[0] not in SHIPPABLE:
            blocked.append((path, entry[0]))
        else:
            passed += 1
 
    for path in unregistered:
        print(f"unregistered: {path}")
    for path, origin in blocked:
        print(f"not shippable ({origin}): {path}")
    print(f"passed {passed} / unregistered {len(unregistered)} / blocked {len(blocked)}")
    return 1 if unregistered or blocked else 0
 
 
if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.exit("usage: audit.py <ship_dir> <ledger.tsv>")
    sys.exit(main(sys.argv[1], sys.argv[2]))

Three notes on why it is written this way.

First, SHIPPABLE is a set. Right now it holds only hand, but a day will come when I add purchased material as licensed. When that happens I would rather add one word to a set than rewrite a condition. Keeping the criterion in one place is a kindness to whoever reads this next, which is usually me.

Second, unregistered and blocked are counted separately. Both stop the build, but they call for different actions. Unregistered means a step was skipped — check it, register it, move on. Blocked means a judgment went wrong, and nothing should proceed until that file leaves the ship folder. Collapsing them into a single count throws that distinction away.

Third, the file is read in 1 MiB chunks. Full-length pieces get large, and I would rather not load an entire library into memory to fingerprint it.

The numbers I got on my own machine

I first ran it against a layout close to the candidate folder I described at the start: 19 audio files, 17 of them registered.

unregistered: ship/bgm/calm_13_new.wav
not shippable (generated): ship/bgm/draft_lyria_take3.wav
passed 17 / unregistered 1 / blocked 1

Exit code 1. The two files I could no longer tell apart by name were stopped for two different reasons. Those two lines were exactly what I wanted.

I timed it as well. Against 40 tracks of 30-second 44.1kHz stereo audio, 203 MB in total, it took 0.50 seconds on all three runs. Hashing reads every byte, so the time scales with total size rather than file count. Even for a library of a few gigabytes, running it once right before an export is not a wait worth optimizing away.

I put it immediately before the shipping export starts. Noticing afterwards means rebuilding the artifact.

Inspecting the audio itself is a separate stage. For things that can be settled numerically without listening — loop seams, for instance — I wrote that up in Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini. Keeping the "where did this come from" check apart from the "is this good" check keeps both of them short.

Not shutting generation out, but giving it a destination

One thing I would rather not be misread on: I have not decided against using generated audio. I have only separated the inside of the shipped product from the outside.

Generated takes now live in three places. One is screen prototypes, where they stand in as placeholder audio while I settle length and structure. Another is as a rough draft when I am assembling something myself, to see one possible shape of the arrangement. The third is as a comparison point when I want to check whether an existing track has gone flat. None of those reach a listener's device.

The models on the Gemini side have shifted generations several times in the past few weeks alone. On how a bill can grow even when the per-token price stays put, I wrote Gemini 3.8 Flash Costs the Same Per Token and Can Still Raise Your Bill. Fixing the boundary in place is what keeps me from redoing the judgment every time a model changes underneath it.

If you try one thing today, scanning your ship folder once and re-registering it by hash instead of by filename is enough. That is where I started too.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◈ API / SDK2026-10-14
When Your config Raises "Extra inputs are not permitted", Check the Spelling of Every Key
If a google-genai call fails with a pydantic ValidationError (extra_forbidden) the moment you pass a config, it is almost always a one-letter typo. Here is the output I reproduced, a small function that checks keys before the call, and how to tell why response.text came back as None.
◈ API / SDK2026-10-08
When a Deprecation Has No Shutdown Date: Re-Picking an Image Model on Three Conditions
gemini-nano-banana-2.1 went GA on October 6, and gemini-3.1-flash-image was deprecated with no shutdown date. Here is a table and a short script for choosing a replacement by resolution, reference images, and search grounding.
◈ API / SDK2026-09-29
The Size Column Said 'F6' and 'About A4' — I Let Gemini Read It, and Let a Table Do the Math
An artist's spreadsheet mixed 'F6', '41×31.8cm', and 'about A4' in one size column. Here is the small tool I built: Gemini reads each cell, a lookup table converts to centimeters, and the artwork image decides the orientation. Working code, step by step.