diff --git a/CHANGELOG.md b/CHANGELOG.md index 15012a23..7c2f0367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,12 +90,13 @@ because it turns other people's test suites red. rest without a word, and the set still expected such a workbook to be accepted, certain. Past that width `sheet.xlsx` is now `unspecified` for `count_limit`, like the wide CSV beside it. Up to it, nothing changes. -- **A picture too low for its label no longer says it carries one.** The - label is drawn as large as the width allows, so a low picture cuts it off, - and a BMP 512 px wide and 1 px tall said `label_embedded: true` with no - text in it at all. Every image format and both film formats now say - `false` with a `label_omitted` note when the text is not whole in the - picture. The pixels are the same as before. +- **A picture whose label cannot be read no longer says it carries one.** + The label is drawn as large as the width allows, so a low picture cuts it + off, and a BMP 512 px wide and 1 px tall said `label_embedded: true` with + no text in it. A low, wide film of one second drew its moving square over + the label. Every image format and both film formats now say `false` with a + `label_omitted` note when no picture shows the whole text. The pixels are + the same as before. - **`tfg cleanup --yes` keeps a file that changed after it was checked.** Cleanup checks every file before it removes any, and a file changed in that time was removed without `--force`. It now looks again just before it diff --git a/internal/format/imagelabel/imagelabel.go b/internal/format/imagelabel/imagelabel.go index 67fd17cc..114238d0 100644 --- a/internal/format/imagelabel/imagelabel.go +++ b/internal/format/imagelabel/imagelabel.go @@ -154,6 +154,55 @@ func CellsOf(width, chars int) Cells { return Cells{First: pad, Step: (glyphWidth + 1) * s, Width: glyphWidth * s} } +// TextRect is where Draw puts the text of a label of chars characters across +// width, in a picture tall enough to hold it: from the left edge of the first +// character to the right edge of the last, over the rows of the glyphs - the +// band's padding is not in it. The empty rectangle when Draw would draw +// nothing. A film asks it to know whether the square it draws over the label +// leaves the text whole (Hides). +func TextRect(width, chars int) image.Rectangle { + c := CellsOf(width, chars) + if c.Width == 0 { + return image.Rectangle{} + } + return image.Rect(c.First, pad, c.First+(chars-1)*c.Step+c.Width, textHeight(width, chars)) +} + +// Hides says whether r, painted in the label's ink over the text Draw puts +// across width, changes a pixel of that text - a pixel of the background +// inside it, between the glyphs or within them. Ink over the glyphs' own ink +// changes nothing, and the band around the text is not the text, the rule +// Readable keeps for the padding at the foot: a square over the last column +// of a 7 and the band beside it leaves the label as it was (measured on the +// pixels, 2026-11-03). +func Hides(width int, text string, r image.Rectangle) bool { + t := laidText{Cells: CellsOf(width, len(text)), scale: scaleFor(width, len(text)), runes: []rune(text)} + in := r.Intersect(TextRect(width, len(text))) + // Draw spaces the text by its bytes and draws its runes, so no glyph + // stands past the last rune. + in.Max.X = min(in.Max.X, t.First+(len(t.runes)-1)*t.Step+t.Width) + for i := range max(in.Dx(), 0) * in.Dy() { + if !t.ink(in.Min.X+i%in.Dx(), in.Min.Y+i/in.Dx()) { + return true + } + } + return false +} + +// laidText is a text where Draw lays it out, asked about its pixels. +type laidText struct { + Cells + scale int + runes []rune +} + +// ink says whether the pixel at x, y inside the text is the ink of a glyph, +// rather than the background between the glyphs or within one. +func (t laidText) ink(x, y int) bool { + k, off := (x-t.First)/t.Step, (x-t.First)%t.Step + return off < t.Width && glyph(t.runes[k])[(y-pad)/t.scale*glyphWidth+off/t.scale] == '#' +} + // BandHeight is how tall the band Draw paints for a label of chars characters // across width is, before it is cut to the picture - 0 when the label does not // fit. A film draws a second line under the first and asks this before it diff --git a/internal/format/video/choose.go b/internal/format/video/choose.go index 42642a84..5f2ffda9 100644 --- a/internal/format/video/choose.go +++ b/internal/format/video/choose.go @@ -73,15 +73,30 @@ type Choice struct { Named bool } -// Labelled is whether this picture carries its label where it can be read: -// drawn across its width, and whole in its height. The manifest says this, -// and the drawing asks the package's Labelled, which is the width alone - a -// low film draws its label and cuts it off (R14 of -// docs/STABILITY-REVIEW-2026-10-26.md, where the pixels stayed as they were). -func (c Choice) Labelled() bool { +// Labelled is whether this film carries its label where it can be read: +// drawn across its width, whole in its height, and whole in at least one of +// its pictures under the square. The manifest says this, and the drawing asks +// the package's Labelled, which is the width alone - a low film draws its +// label and cuts it off (R14 of docs/STABILITY-REVIEW-2026-10-26.md), and a +// low wide one of few changes draws the square over it (O288). The pixels +// stayed as they were both times. +func (c Choice) Labelled(t Timeline) bool { + return c.readable() && !c.squareOverLabel(t) +} + +// readable is whether the label is drawn and whole in the picture's height, +// before the square is drawn over it. +func (c Choice) readable() bool { return Labelled(c.Width, c.Label) && imagelabel.Readable(c.Width, c.Height, len(c.Label)) } +// squareOverLabel is whether the square crosses the text of a label drawn +// whole in every picture of the film - the one case the plan owes a note of +// its own for. +func (c Choice) squareOverLabel(t Timeline) bool { + return c.readable() && !geometryOf(c.Width, c.Height, c.Label, t).labelWholeIn(t.Changes(), c.Label) +} + // ClockShown is whether this film's pictures show the clock. func (c Choice) ClockShown(t Timeline) bool { return ClockShown(c.Width, c.Height, c.Label, t) } @@ -292,6 +307,6 @@ func Facts(c Choice, s Settings) map[string]any { "quality": s.Quality, "compression": "av1", "audio": false, - format.PropertyLabelEmbedded: c.Labelled(), + format.PropertyLabelEmbedded: c.Labelled(s.Timeline), } } diff --git a/internal/format/video/picture.go b/internal/format/video/picture.go index 4073a421..e5ab2c37 100644 --- a/internal/format/video/picture.go +++ b/internal/format/video/picture.go @@ -131,6 +131,28 @@ func (g geometry) squareX(c int64) int { return (g.width - g.side) * int(c%squareSteps) / (squareSteps - 1) } +// squareAt is the square standing at column x - what a picture paints, and +// what the label is asked about, so the two cannot come apart. +func (g geometry) squareAt(x int) image.Rectangle { + return image.Rect(x, g.squareY, x+g.side, g.squareY+g.side) +} + +// labelWholeIn says whether the text of the label is whole in at least one +// picture of a film of this many changes - the square, drawn over it in its +// ink, changing none of it there. It stands in squareSteps places, so a film +// of that many changes or more stands in all of them, and a film of one change +// only at the left edge, over the start of the label of a low picture (O288 of +// docs/OBSERVATIONS.md). One whole picture is enough, by the owner's decision +// of 2026-10-27: it is the one that identifies the file. +func (g geometry) labelWholeIn(changes int64, label string) bool { + for c := range min(changes, squareSteps) { + if !imagelabel.Hides(g.width, label, g.squareAt(g.squareX(c))) { + return true + } + } + return false +} + // clockRight is the column after the clock's last character, nought without // a clock. func (g geometry) clockRight() int { @@ -350,7 +372,7 @@ func (f *film) whole(l look) Planes { b := work.Rect imagelabel.Draw(work.SubImage(image.Rect(0, f.clockFrom, b.Dx(), b.Dy())).(*image.RGBA), l.clock) } - draw.Draw(work, image.Rect(l.x, f.squareY, l.x+f.side, f.squareY+f.side), f.square, image.Point{}, draw.Src) + draw.Draw(work, f.squareAt(l.x), f.square, image.Point{}, draw.Src) planes, _ := newPlanes(f.width, f.height) toPlanes(work, planes) return planes @@ -419,7 +441,7 @@ func (p *painter) drawIn(l look, r image.Rectangle) Planes { if f.showClock() && s.Rect.Min.Y <= f.clockFrom && f.clockFrom < s.Rect.Max.Y { imagelabel.Draw(s.SubImage(image.Rect(0, f.clockFrom, f.width, s.Rect.Max.Y)).(*image.RGBA), l.clock) } - draw.Draw(s, image.Rect(l.x, f.squareY, l.x+f.side, f.squareY+f.side), f.square, image.Point{}, draw.Src) + draw.Draw(s, f.squareAt(l.x), f.square, image.Point{}, draw.Src) convertRect(s, out, r.Min, in) } return out diff --git a/internal/format/video/plan.go b/internal/format/video/plan.go index 29c1afc6..70ca5d52 100644 --- a/internal/format/video/plan.go +++ b/internal/format/video/plan.go @@ -63,6 +63,13 @@ func Plan(formatID string, r format.Request, need func(Stream) int64, refuse fun // The answer is c.Labelled, which Facts has already put in the plan. This // is the note a label drawn and cut off at the foot of the picture owes. _, p.Notes = imagelabel.Claim(r.Label && Labelled(c.Width, c.Label), c.Width, c.Height, len(c.Label), p.Notes) + if c.squareOverLabel(s.Timeline) { + p.Notes = append(p.Notes, format.Note{ + Code: "label_omitted", + Detail: core.Says("video.TheSquareCoversTheLabel", "The picture is %dx%d and the square that steps across it covers part of the label in every picture, so this file carries no readable label. Its name and the manifest still identify it.", + core.A("Width", c.Width), core.A("Height", c.Height)), + }) + } if !c.ClockShown(s.Timeline) { p.Notes = append(p.Notes, format.Note{ Code: "clock_omitted", diff --git a/internal/guard/labelclaim_test.go b/internal/guard/labelclaim_test.go index a5322186..5549de11 100644 --- a/internal/guard/labelclaim_test.go +++ b/internal/guard/labelclaim_test.go @@ -8,6 +8,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" + "github.com/donislawdev/TestingFilesGenerator/internal/format/video" ) // A picture says it carries its label only when the text of the label is whole @@ -102,6 +103,177 @@ func TestALabelIsReadableFromTheFirstHeightThatHoldsItsText(t *testing.T) { } } +// A film says it carries its label only when one of its pictures shows the +// text whole under the square. +// +// The square steps across a film in ten places and is drawn over the label in +// the label's own ink. On a low wide film it rides across the text, and until +// 2026-11-03 a film of 1920x64 and one change drew it over "tfg -" in its only +// picture and said label_embedded true (O288 of docs/OBSERVATIONS.md). Asked +// of the pixels: the text rows of every picture are compared with the same +// picture of the film made tall, where the square passes under the label, over +// the columns the label's ink covers - measured from the label drawn on its +// own. Across heights at three scales and widths around the one where the +// second of two pictures first clears the text, so the answer is held at both +// edges it can move at, and each series has to give both answers. +func TestAFilmSaysItCarriesItsLabelOnlyWhenAPictureShowsItWhole(t *testing.T) { + const seed, tall = 7, 256 + label := core.Label("webm", 1<<20, seed) + across := len(label)*4 + 4 // the width that draws it at scale 1, as in the guard above + timeline := func(changes int64) video.Timeline { + tl, err := video.NewTimeline("webm", changes*1000, 60_000, 1000, 30) + if err != nil { + t.Fatal(err) + } + return tl + } + // shown says whether picture c of the film leaves the label's ink as the + // tall film shows it, over every row and column the ink reaches. The tall + // picture is the same at every height, so it is painted once. + talls := map[[2]int64]video.Planes{} + shown := func(w, h int, tl video.Timeline, ink image.Rectangle, c int64) bool { + got := video.WholePicture(w, h, seed, label, tl, c) + key := [2]int64{int64(w)*1000 + tl.Changes(), c} + want, ok := talls[key] + if !ok { + want = video.WholePicture(w, tall, seed, label, tl, c) + talls[key] = want + } + for y := ink.Min.Y; y < ink.Max.Y; y++ { + for x := ink.Min.X; x < ink.Max.X; x++ { + if got.Y[y*w+x] != want.Y[y*w+x] { + return false + } + } + } + return true + } + ask := func(w, h int, tl video.Timeline) (said, seen bool, compared bool) { + said = video.Choice{Width: w, Height: h, Seed: seed, Label: label}.Labelled(tl) + if !video.Labelled(w, label) || !imagelabel.Readable(w, h, len(label)) { + return said, false, false + } + ink := labelInkBounds(w, label) + if text := imagelabel.TextRect(w, len(label)); ink != text { + t.Fatalf("at %d px across the label's ink covers %v and TextRect says %v - this guard reads the ink and the film asks TextRect, so the two have to agree on its label", w, ink, text) + } + for c := range tl.Changes() { + if shown(w, h, tl, ink, c) { + return said, true, true + } + } + return said, false, true + } + type series struct { + name string + cases [][2]int + tl video.Timeline + } + var all []series + for _, changes := range []int64{1, 2, 10} { + for _, scale := range []int{1, 2, 4} { + s := series{name: "scale " + strconv.Itoa(scale) + ", " + strconv.FormatInt(changes, 10) + " change(s), heights", tl: timeline(changes)} + for h := 1; h <= 120; h++ { + s.cases = append(s.cases, [2]int{across * scale, h}) + } + all = append(all, s) + } + } + // The square of the second of two pictures stands a ninth of the way along + // the room it leaves, so at 64 px tall, where it is 64 px and rides over + // the text, a film 9 x + 64 px wide has it at column x. Walked over the + // last two characters and past the text's right edge, which is where it + // stops hiding anything - not at the edge, because the last column of a 7 + // is ink all the way down. + right := imagelabel.TextRect(across*4, len(label)).Max.X + step := imagelabel.CellsOf(across*4, len(label)).Step + edge := series{name: "2 changes at 64 px, widths", tl: timeline(2)} + for x := right - 2*step; x <= right+2; x++ { + edge.cases = append(edge.cases, [2]int{9*x + 64, 64}) + } + all = append(all, edge) + + for _, s := range all { + answers := map[bool]int{} + for _, wh := range s.cases { + said, seen, compared := ask(wh[0], wh[1], s.tl) + if !compared { + if said { + t.Errorf("%s: a film of %dx%d says it carries its label, and the label is not whole in its height", s.name, wh[0], wh[1]) + } + continue + } + answers[seen]++ + if said != seen { + t.Errorf("%s: a film of %dx%d says label_embedded %v, and a picture of it shows the label whole: %v", s.name, wh[0], wh[1], said, seen) + } + } + if answers[true] == 0 || answers[false] == 0 { + t.Errorf("%s: the pictures showed the label whole %d time(s) and covered %d - the series never reached the edge it is there for", s.name, answers[true], answers[false]) + } + } +} + +// And the plan of both film formats says it, with a note when the square is +// what hides the label: the film of O288, and the same film at the default +// ten changes, whose last picture shows the label whole. +func TestAFilmWhoseSquareCoversItsLabelSaysSoInItsPlan(t *testing.T) { + asked := 0 + for _, id := range format.IDs() { + d, err := format.Get(id) + if err != nil { + t.Fatal(err) + } + if _, ok := settingNamed(d, video.SettingChangeInterval); !ok { + continue + } + asked++ + for _, c := range []struct { + duration string + want bool + }{{"1s", false}, {"10s", true}} { + props := map[string]string{"width": "1920", "height": "64", video.SettingDuration: c.duration} + r := format.Request{Seed: 1, Label: true, Properties: props} + r.Bytes = format.SmallestRemembered(d, r) + 64<<10 + p, err := d.Generator.Plan(r) + if err != nil { + t.Fatalf("%s 1920x64 for %s: %v", id, c.duration, err) + } + embedded, _ := p.Properties[format.PropertyLabelEmbedded].(bool) + omitted := 0 + for _, n := range p.Notes { + if n.Code == "label_omitted" { + omitted++ + } + } + if embedded != c.want || omitted != map[bool]int{true: 0, false: 1}[c.want] { + t.Errorf("%s 1920x64 for %s says label_embedded %v with %d label_omitted note(s), want %v with %d - notes %v", + id, c.duration, embedded, omitted, c.want, map[bool]int{true: 0, false: 1}[c.want], p.Notes) + } + } + } + if asked < 2 { + t.Fatalf("only %d format(s) declare %s - the registry was not read", asked, video.SettingChangeInterval) + } +} + +// labelInkBounds is the rectangle the pixels of a label drawn alone across +// width cover, read from the pixels. +func labelInkBounds(width int, label string) image.Rectangle { + img := image.NewRGBA(image.Rect(0, 0, width, 64)) + imagelabel.Draw(img, label) + band := img.RGBAAt(0, 0) + var ink image.Rectangle + for y := range 64 { + for x := range width { + if c := img.RGBAAt(x, y); c != band && c.A != 0 { + ink = ink.Union(image.Rect(x, y, x+1, y+1)) + } + } + } + return ink +} + func hasNote(notes []format.Note, code string) bool { for _, n := range notes { if n.Code == code { diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index 8db2edeb..9f90f5fa 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -12316,8 +12316,8 @@ ( "the installer job runs on Linux, where there is nothing to install", ".github/workflows/ci.yml", - " runs-on: windows-latest\n timeout-minutes: 20\n steps:\n - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n persist-credentials: false\n\n - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0\n with:\n python-version: \"3.14\"\n\n - name: build the installer twice", - " runs-on: ubuntu-latest\n timeout-minutes: 20\n steps:\n - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n with:\n persist-credentials: false\n\n - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0\n with:\n python-version: \"3.14\"\n\n - name: build the installer twice", + " # virtual machine instead, 0.3.0 to 0.4.0 (tools/packaging-vm).\n runs-on: windows-latest\n", + " # virtual machine instead, 0.3.0 to 0.4.0 (tools/packaging-vm).\n runs-on: ubuntu-latest\n", "TestTheInstallerIsInstalledOnARunner", ), ( @@ -13673,7 +13673,7 @@ # picture from the whole painter, which draws it. "a film's strip painter leaves the square out", "internal/format/video/picture.go", - " draw.Draw(s, image.Rect(l.x, f.squareY, l.x+f.side, f.squareY+f.side), f.square, image.Point{}, draw.Src)", + " draw.Draw(s, f.squareAt(l.x), f.square, image.Point{}, draw.Src)", " _ = f.square", "TestAFilmPaintsOnlyWhatChangesAndGetsTheWholePicture", ), @@ -16361,4 +16361,48 @@ "\t_ = engine.OwnProcess\n", "TestTheWindowSaysItsProcessIsItsOwnBeforeItOpens", ), + # O288 (docs/OBSERVATIONS.md): a film says it carries its label only when + # one of its pictures shows the text whole under the square. + ( + "a film says it carries a label the square covers in every picture", + "internal/format/video/picture.go", + "if !imagelabel.Hides(g.width, label, g.squareAt(g.squareX(c))) {", + "if !imagelabel.Hides(g.width, label, g.squareAt(g.squareX(c))) || true {", + "TestAFilmWhoseSquareCoversItsLabelSaysSoInItsPlan", + ), + ( + "a film asks only its first picture whether the label is whole", + "internal/format/video/picture.go", + "for c := range min(changes, squareSteps) {", + "for c := range min(changes, 1) {", + "TestAFilmWhoseSquareCoversItsLabelSaysSoInItsPlan", + ), + ( + "the square over the glyphs' own ink is said to hide the label", + "internal/format/imagelabel/imagelabel.go", + "return off < t.Width && glyph(t.runes[k])[(y-pad)/t.scale*glyphWidth+off/t.scale] == '#'", + "return off < t.Width && glyph(t.runes[k])[(y-pad)/t.scale*glyphWidth+off/t.scale] == '#' && false", + "TestAFilmSaysItCarriesItsLabelOnlyWhenAPictureShowsItWhole", + ), + ( + "the rectangle of a label's text ends a column short of its ink", + "internal/format/imagelabel/imagelabel.go", + "return image.Rect(c.First, pad, c.First+(chars-1)*c.Step+c.Width, textHeight(width, chars))", + "return image.Rect(c.First, pad, c.First+(chars-1)*c.Step+c.Width-1, textHeight(width, chars))", + "TestAFilmSaysItCarriesItsLabelOnlyWhenAPictureShowsItWhole", + ), + ( + "a film whose square covers its label gives no note", + "internal/format/video/plan.go", + "if c.squareOverLabel(s.Timeline) {", + "if false {", + "TestAFilmWhoseSquareCoversItsLabelSaysSoInItsPlan", + ), + ( + "the manifest of a film is not asked about the square", + "internal/format/video/choose.go", + "return c.readable() && !c.squareOverLabel(t)", + "return c.readable()", + "TestAFilmWhoseSquareCoversItsLabelSaysSoInItsPlan", + ), ] diff --git a/internal/gui/text/locale/said/en.json b/internal/gui/text/locale/said/en.json index 0654f29e..9247585c 100644 --- a/internal/gui/text/locale/said/en.json +++ b/internal/gui/text/locale/said/en.json @@ -3741,6 +3741,11 @@ "hash": "sha256-e6b01f8c0aeb", "other": "at {{.FPS}} frames a second a length has to end on a frame, and lengths at that rate go in steps of {{.Step}}. Ask for {{.Above}}, the shortest film at this rate" }, + "video.TheSquareCoversTheLabel": { + "description": "Said by internal/format/video - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, {{.Height}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c37dab3cb322", + "other": "The picture is {{.Width}}x{{.Height}} and the square that steps across it covers part of the label in every picture, so this file carries no readable label. Its name and the manifest still identify it." + }, "video.TooManyChanges": { "description": "Said by internal/format/video - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Duration}}, {{.Interval}}, {{.Pictures}}, {{.Most}}, {{.Shortest}}, written the way the program writes them - keep each spelled exactly that way.", "hash": "sha256-79ad4cf6e19b", diff --git a/internal/gui/text/locale/said/pl.json b/internal/gui/text/locale/said/pl.json index 6a43cba2..663c1ae0 100644 --- a/internal/gui/text/locale/said/pl.json +++ b/internal/gui/text/locale/said/pl.json @@ -745,6 +745,7 @@ "video.NoClock": { "hash": "sha256-9fb32f58b247", "other": "Obraz ma {{.Width}}x{{.Height}} i zegar potrzebuje więcej miejsca, więc ten film nie pokazuje zegara. Kwadrat dalej przesuwa się po nim tam, gdzie ma miejsce na ruch." }, "video.NotOnAFrame": { "hash": "sha256-4471642f4d67", "other": "przy {{.FPS}} klatkach na sekundę długość musi kończyć się na klatce, a przy tej szybkości długości idą co {{.Step}}. Poproś o {{.Below}} albo {{.Above}}" }, "video.NotOnAFrameShortest": { "hash": "sha256-e6b01f8c0aeb", "other": "przy {{.FPS}} klatkach na sekundę długość musi kończyć się na klatce, a przy tej szybkości długości idą co {{.Step}}. Poproś o {{.Above}}, najkrótszy film przy tej szybkości" }, + "video.TheSquareCoversTheLabel": { "hash": "sha256-c37dab3cb322", "other": "Obraz ma {{.Width}}x{{.Height}}, a kwadrat, który przesuwa się po nim, zasłania część etykiety w każdym obrazie, więc ten plik nie niesie czytelnej etykiety. Wciąż wskazują go jego nazwa i manifest." }, "video.TooManyChanges": { "hash": "sha256-79ad4cf6e19b", "other": "liczba obrazów w filmie o długości {{.Duration}} z nowym obrazem co {{.Interval}} to {{.Pictures}}, a film może pokazać najwyżej {{.Most}}. Poproś o {{.Shortest}} albo dłużej albo o krótszy film" }, "video.TooManyKeyFrames": { "hash": "sha256-fab3e045a284", "other": "film o długości {{.Duration}} z klatką kluczową co {{.Interval}} ma ich {{.Keys}}, a film może mieć najwyżej {{.Most}}. Poproś o {{.Shortest}} albo dłużej albo o krótszy film" }, "wav.DropTheLabel": { "hash": "sha256-b8bfddcdab7d", "other": " albo usuń etykietę" },