schnippo 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. schnippo-0.11.0/PKG-INFO +208 -0
  2. schnippo-0.11.0/README.md +177 -0
  3. schnippo-0.11.0/pyproject.toml +61 -0
  4. schnippo-0.11.0/pyproject.toml.orig +56 -0
  5. schnippo-0.11.0/src/schnippo/__init__.py +3 -0
  6. schnippo-0.11.0/src/schnippo/acoustic.py +166 -0
  7. schnippo-0.11.0/src/schnippo/analysis.py +183 -0
  8. schnippo-0.11.0/src/schnippo/audio.py +267 -0
  9. schnippo-0.11.0/src/schnippo/branding.py +26 -0
  10. schnippo-0.11.0/src/schnippo/cache.py +61 -0
  11. schnippo-0.11.0/src/schnippo/cache_admin.py +94 -0
  12. schnippo-0.11.0/src/schnippo/config.py +181 -0
  13. schnippo-0.11.0/src/schnippo/context.py +149 -0
  14. schnippo-0.11.0/src/schnippo/cutlist.py +1358 -0
  15. schnippo-0.11.0/src/schnippo/data/guides/01_rough_cut.md +69 -0
  16. schnippo-0.11.0/src/schnippo/data/guides/02_fine_cut.md +45 -0
  17. schnippo-0.11.0/src/schnippo/data/guides/03_broll.md +39 -0
  18. schnippo-0.11.0/src/schnippo/data/guides/04_polish.md +40 -0
  19. schnippo-0.11.0/src/schnippo/data/guides/05_motion_graphics.md +63 -0
  20. schnippo-0.11.0/src/schnippo/data/guides/06_sound_design.md +31 -0
  21. schnippo-0.11.0/src/schnippo/data/guides/07_reels.md +30 -0
  22. schnippo-0.11.0/src/schnippo/data/guides/design_tokens.json +124 -0
  23. schnippo-0.11.0/src/schnippo/data/guides/technik.md +86 -0
  24. schnippo-0.11.0/src/schnippo/data/sfx/LICENSES.md +54 -0
  25. schnippo-0.11.0/src/schnippo/data/sfx/analysis/groups.json +974 -0
  26. schnippo-0.11.0/src/schnippo/data/sfx/library.json +860 -0
  27. schnippo-0.11.0/src/schnippo/data/sfx/recorded.json +289 -0
  28. schnippo-0.11.0/src/schnippo/data/sfx/wav/alarm_bell.wav +0 -0
  29. schnippo-0.11.0/src/schnippo/data/sfx/wav/alarm_real.wav +0 -0
  30. schnippo-0.11.0/src/schnippo/data/sfx/wav/click_mouse.wav +0 -0
  31. schnippo-0.11.0/src/schnippo/data/sfx/wav/click_mouse_sharp.wav +0 -0
  32. schnippo-0.11.0/src/schnippo/data/sfx/wav/click_mouse_soft.wav +0 -0
  33. schnippo-0.11.0/src/schnippo/data/sfx/wav/clock_timer.wav +0 -0
  34. schnippo-0.11.0/src/schnippo/data/sfx/wav/clock_timer_b.wav +0 -0
  35. schnippo-0.11.0/src/schnippo/data/sfx/wav/ding_bell.wav +0 -0
  36. schnippo-0.11.0/src/schnippo/data/sfx/wav/ding_bright.wav +0 -0
  37. schnippo-0.11.0/src/schnippo/data/sfx/wav/ding_chime.wav +0 -0
  38. schnippo-0.11.0/src/schnippo/data/sfx/wav/ding_glass.wav +0 -0
  39. schnippo-0.11.0/src/schnippo/data/sfx/wav/ding_notify.wav +0 -0
  40. schnippo-0.11.0/src/schnippo/data/sfx/wav/ding_real.wav +0 -0
  41. schnippo-0.11.0/src/schnippo/data/sfx/wav/downer_tape.wav +0 -0
  42. schnippo-0.11.0/src/schnippo/data/sfx/wav/downer_vinyl.wav +0 -0
  43. schnippo-0.11.0/src/schnippo/data/sfx/wav/impact_boom.wav +0 -0
  44. schnippo-0.11.0/src/schnippo/data/sfx/wav/impact_hit.wav +0 -0
  45. schnippo-0.11.0/src/schnippo/data/sfx/wav/impact_soft_low.wav +0 -0
  46. schnippo-0.11.0/src/schnippo/data/sfx/wav/impact_soft_mid.wav +0 -0
  47. schnippo-0.11.0/src/schnippo/data/sfx/wav/pop_bright.wav +0 -0
  48. schnippo-0.11.0/src/schnippo/data/sfx/wav/pop_mid.wav +0 -0
  49. schnippo-0.11.0/src/schnippo/data/sfx/wav/pop_soft.wav +0 -0
  50. schnippo-0.11.0/src/schnippo/data/sfx/wav/riser_impact.wav +0 -0
  51. schnippo-0.11.0/src/schnippo/data/sfx/wav/riser_long.wav +0 -0
  52. schnippo-0.11.0/src/schnippo/data/sfx/wav/riser_short.wav +0 -0
  53. schnippo-0.11.0/src/schnippo/data/sfx/wav/riser_swell.wav +0 -0
  54. schnippo-0.11.0/src/schnippo/data/sfx/wav/swish_bright.wav +0 -0
  55. schnippo-0.11.0/src/schnippo/data/sfx/wav/swish_mid.wav +0 -0
  56. schnippo-0.11.0/src/schnippo/data/sfx/wav/swish_soft.wav +0 -0
  57. schnippo-0.11.0/src/schnippo/data/sfx/wav/tick_bright.wav +0 -0
  58. schnippo-0.11.0/src/schnippo/data/sfx/wav/tick_mid.wav +0 -0
  59. schnippo-0.11.0/src/schnippo/data/sfx/wav/tick_soft.wav +0 -0
  60. schnippo-0.11.0/src/schnippo/data/sfx/wav/typing_mech.wav +0 -0
  61. schnippo-0.11.0/src/schnippo/data/sfx/wav/typing_mech_b.wav +0 -0
  62. schnippo-0.11.0/src/schnippo/data/sfx/wav/whoosh_big.wav +0 -0
  63. schnippo-0.11.0/src/schnippo/data/sfx/wav/whoosh_end.wav +0 -0
  64. schnippo-0.11.0/src/schnippo/data/sfx/wav/whoosh_mid.wav +0 -0
  65. schnippo-0.11.0/src/schnippo/data/sfx/wav/whoosh_soft.wav +0 -0
  66. schnippo-0.11.0/src/schnippo/delivery.py +237 -0
  67. schnippo-0.11.0/src/schnippo/design.py +78 -0
  68. schnippo-0.11.0/src/schnippo/edges.py +121 -0
  69. schnippo-0.11.0/src/schnippo/errors.py +63 -0
  70. schnippo-0.11.0/src/schnippo/export.py +201 -0
  71. schnippo-0.11.0/src/schnippo/fcpxml.py +72 -0
  72. schnippo-0.11.0/src/schnippo/ffmpeg_dist.py +32 -0
  73. schnippo-0.11.0/src/schnippo/formats.py +149 -0
  74. schnippo-0.11.0/src/schnippo/formatting.py +123 -0
  75. schnippo-0.11.0/src/schnippo/frames.py +272 -0
  76. schnippo-0.11.0/src/schnippo/framing.py +25 -0
  77. schnippo-0.11.0/src/schnippo/glossary.py +182 -0
  78. schnippo-0.11.0/src/schnippo/guard.py +125 -0
  79. schnippo-0.11.0/src/schnippo/guides.py +48 -0
  80. schnippo-0.11.0/src/schnippo/jobs.py +68 -0
  81. schnippo-0.11.0/src/schnippo/kenburns.py +59 -0
  82. schnippo-0.11.0/src/schnippo/keychain.py +35 -0
  83. schnippo-0.11.0/src/schnippo/markers.py +80 -0
  84. schnippo-0.11.0/src/schnippo/media.py +247 -0
  85. schnippo-0.11.0/src/schnippo/mg/__init__.py +0 -0
  86. schnippo-0.11.0/src/schnippo/mg/fonts/dm-sans-600.woff2 +0 -0
  87. schnippo-0.11.0/src/schnippo/mg/fonts/dm-sans-800.woff2 +0 -0
  88. schnippo-0.11.0/src/schnippo/mg/fonts/dm-sans-LICENSE.txt +93 -0
  89. schnippo-0.11.0/src/schnippo/mg/fonts/instrument-sans-600.woff2 +0 -0
  90. schnippo-0.11.0/src/schnippo/mg/fonts/instrument-sans-700.woff2 +0 -0
  91. schnippo-0.11.0/src/schnippo/mg/fonts/instrument-sans-LICENSE.txt +93 -0
  92. schnippo-0.11.0/src/schnippo/mg/fonts/inter-600.woff2 +0 -0
  93. schnippo-0.11.0/src/schnippo/mg/fonts/inter-800.woff2 +0 -0
  94. schnippo-0.11.0/src/schnippo/mg/fonts/inter-LICENSE.txt +93 -0
  95. schnippo-0.11.0/src/schnippo/mg/fonts/manrope-600.woff2 +0 -0
  96. schnippo-0.11.0/src/schnippo/mg/fonts/manrope-800.woff2 +0 -0
  97. schnippo-0.11.0/src/schnippo/mg/fonts/manrope-LICENSE.txt +93 -0
  98. schnippo-0.11.0/src/schnippo/mg/fonts/space-grotesk-600.woff2 +0 -0
  99. schnippo-0.11.0/src/schnippo/mg/fonts/space-grotesk-700.woff2 +0 -0
  100. schnippo-0.11.0/src/schnippo/mg/fonts/space-grotesk-LICENSE.txt +93 -0
  101. schnippo-0.11.0/src/schnippo/mg/place.py +127 -0
  102. schnippo-0.11.0/src/schnippo/mg/preview.py +142 -0
  103. schnippo-0.11.0/src/schnippo/mg/render.py +254 -0
  104. schnippo-0.11.0/src/schnippo/mg/templates.py +331 -0
  105. schnippo-0.11.0/src/schnippo/mg/tokens.py +133 -0
  106. schnippo-0.11.0/src/schnippo/migrate.py +38 -0
  107. schnippo-0.11.0/src/schnippo/models.py +130 -0
  108. schnippo-0.11.0/src/schnippo/onboarding.py +120 -0
  109. schnippo-0.11.0/src/schnippo/passes.py +188 -0
  110. schnippo-0.11.0/src/schnippo/prefs.py +103 -0
  111. schnippo-0.11.0/src/schnippo/profiles.py +163 -0
  112. schnippo-0.11.0/src/schnippo/projects.py +129 -0
  113. schnippo-0.11.0/src/schnippo/py.typed +0 -0
  114. schnippo-0.11.0/src/schnippo/qc.py +329 -0
  115. schnippo-0.11.0/src/schnippo/reels.py +238 -0
  116. schnippo-0.11.0/src/schnippo/reframe.py +296 -0
  117. schnippo-0.11.0/src/schnippo/render.py +123 -0
  118. schnippo-0.11.0/src/schnippo/resolve_bridge.py +1081 -0
  119. schnippo-0.11.0/src/schnippo/retakes.py +127 -0
  120. schnippo-0.11.0/src/schnippo/revise.py +479 -0
  121. schnippo-0.11.0/src/schnippo/script.py +79 -0
  122. schnippo-0.11.0/src/schnippo/segments.py +89 -0
  123. schnippo-0.11.0/src/schnippo/sense.py +146 -0
  124. schnippo-0.11.0/src/schnippo/server.py +1388 -0
  125. schnippo-0.11.0/src/schnippo/setup_check.py +184 -0
  126. schnippo-0.11.0/src/schnippo/sfx.py +380 -0
  127. schnippo-0.11.0/src/schnippo/sfx_profile.py +250 -0
  128. schnippo-0.11.0/src/schnippo/sfx_record.py +103 -0
  129. schnippo-0.11.0/src/schnippo/sfx_sources.py +108 -0
  130. schnippo-0.11.0/src/schnippo/snap.py +117 -0
  131. schnippo-0.11.0/src/schnippo/subtitles.py +132 -0
  132. schnippo-0.11.0/src/schnippo/toolinfo.py +18 -0
  133. schnippo-0.11.0/src/schnippo/transcribe/__init__.py +0 -0
  134. schnippo-0.11.0/src/schnippo/transcribe/base.py +41 -0
  135. schnippo-0.11.0/src/schnippo/transcribe/gladia.py +188 -0
  136. schnippo-0.11.0/src/schnippo/ui/card.html +289 -0
  137. schnippo-0.11.0/src/schnippo/ui/design.js +168 -0
  138. schnippo-0.11.0/src/schnippo/updates.py +71 -0
  139. schnippo-0.11.0/src/schnippo/workspace.py +1721 -0
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: schnippo
3
+ Version: 0.11.0
4
+ Summary: KI-Rohschnitt für DaVinci Resolve: MCP-Server für Rohschnitte per Chat
5
+ Keywords: mcp,davinci resolve,video editing,rough cut,transcript
6
+ Author: Seitz Dominik Visuals
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: MacOS X
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Natural Language :: German
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Multimedia :: Video :: Non-Linear Editor
15
+ Requires-Dist: httpx>=0.28.1
16
+ Requires-Dist: mcp[cli]>=2.3.0
17
+ Requires-Dist: numpy>=2.5.4
18
+ Requires-Dist: opencv-python-headless>=4.10,<5
19
+ Requires-Dist: pillow>=13.0.0
20
+ Requires-Dist: portable-ffmpeg-full==0.5.0
21
+ Requires-Dist: pydantic>=2.14.2
22
+ Requires-Dist: python-docx>=1.2.0
23
+ Requires-Dist: rapidfuzz>=3.14.6
24
+ Requires-Dist: playwright==1.64.1 ; extra == 'motion'
25
+ Requires-Python: >=3.14, <3.16
26
+ Project-URL: Homepage, https://schnippo.com
27
+ Project-URL: Repository, https://github.com/Seitzdominik/schnippo
28
+ Project-URL: Issues, https://github.com/Seitzdominik/schnippo/issues
29
+ Provides-Extra: motion
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Schnippo
33
+
34
+ **KI-Rohschnitt für DaVinci Resolve.** Beta.
35
+
36
+ <!-- Bild oder GIF mit freigegebenem Material folgt: docs/bilder/schnippo.gif -->
37
+
38
+ ## Was ist Schnippo?
39
+
40
+ Schnippo baut per Chat aus deinen Takes einen Rohschnitt in DaVinci Resolve Studio: Du sagst Claude, welchen Ordner es schneiden soll, und bekommst eine fertige Timeline aus deinen Originalclips. Schnippo misst Sprache, Pausen, Versprecher und Ton, Claude trifft die redaktionellen Entscheidungen, und du behältst in Resolve das letzte Wort. Auf Wunsch kommen B-Roll, Zooms, Grafiken, Sound-Effekte und Reels aus dem langen Video dazu, alles nicht-destruktiv.
41
+
42
+ ## Voraussetzungen
43
+
44
+ - Mac mit Apple Silicon (Windows folgt)
45
+ - DaVinci Resolve Studio 21.1 oder neuer, unter Preferences → System → General „External scripting using“ auf **Local** (die kostenlose Version kann kein externes Scripting)
46
+ - Claude Desktop
47
+ - Eigener [Gladia-Key](https://www.gladia.io) für die Transkription
48
+
49
+ **Kosten:** Schnippo ist kostenlos. Gladia rechnet direkt mit dir ab, grob 0,61 USD je Stunde Audio. Vor jeder Transkription nennt Schnippo die Schätzung. Dazu kommt dein Claude-Abo.
50
+
51
+ ## Installation in 4 Schritten
52
+
53
+ 1. **Download:** Lade die neueste `schnippo-<version>.mcpb` von der [Releases-Seite](https://github.com/Seitzdominik/schnippo/releases).
54
+ 2. **Doppelklick:** Die Datei öffnet sich in Claude Desktop. Bestätige die Installation.
55
+ 3. **Key:** Trage deinen Gladia-Key in den Einstellungen der Erweiterung ein.
56
+ 4. **Claude vollständig neu starten:** Beende Claude über die Menüleiste (Claude → Beenden). Das Fenster zu schließen reicht nicht.
57
+
58
+ Neue Versionen installierst du genauso. Schnippo weist dich darauf hin und aktualisiert sich nie selbst.
59
+
60
+ ## Erster Schnitt
61
+
62
+ Starte einen neuen Chat und schreibe **„Richte mich ein“**. Das Onboarding prüft Resolve und den Key, fragt deine Vorlieben ab und legt auf Wunsch einen Projektordner an:
63
+
64
+ ```text
65
+ 2026-11-07_Mein_Film/
66
+ ├── A-Roll/ Takes mit Sprache
67
+ ├── B-Roll/ Bildmaterial ohne Sprache
68
+ ├── Bilder/ Standbilder, Logos
69
+ ├── Skript/ Skript, falls vorhanden
70
+ ├── brief.md Worum es geht, Format, Länge, Zielgruppe
71
+ └── glossar.txt Namen in richtiger Schreibweise
72
+ ```
73
+
74
+ Spickzettel für den Chat:
75
+
76
+ | Du schreibst | Schnippo macht |
77
+ |---|---|
78
+ | „Schneide Mein_Film“ | Rough Cut als neue Timeline, danach Rückfragen zu Grenzfällen |
79
+ | „Weiter“ | Nächster Durchgang: Fine Cut, B-Roll, Polish |
80
+ | „Mach aus dem Video 3 Reels“ | Hochformat-Reels aus den stärksten Stellen |
81
+ | „Füge dezente Sound-Effekte hinzu“ | Eigene Spur mit Effekten |
82
+ | „Hilfe“ | Überblick über alles Weitere |
83
+
84
+ Jeder Durchgang bekommt eine eigene Timeline. Schnippo überschreibt nichts und verändert deine Originaldateien nie. Alle Tools stehen in der [Referenz](docs/REFERENZ.md).
85
+
86
+ ## Aufnahme-Tipps
87
+
88
+ - **Für Reels in 4K filmen:** Dann bleibt beim Zuschnitt von 16:9 auf 9:16 genug Auflösung.
89
+ - **Versprecher:** Kurz stoppen, deutlich „nochmal“ sagen oder einmal klatschen und den ganzen Satz neu beginnen.
90
+ - **Bild:** Vor dem Sprechen 1 Sekunde ruhig in die Kamera schauen. Alle Takes mit derselben Framerate. B-Roll gern mit doppelter Rate, dann ist Zeitlupe möglich.
91
+ - **Ton:** Ruhiger Raum, 2–3 Sekunden Raumton am Anfang. Im Interview ein Mikro pro Person auf eigenem Kanal.
92
+ - **Namen:** Schreibweisen in die `glossar.txt`, dann stimmen sie schon im Transkript.
93
+
94
+ Mehr dazu in der [Referenz](docs/REFERENZ.md#so-nimmst-du-für-schnippo-auf), eigene Sound-Effekte aufnehmen: [docs/AUFNAHMEN.md](docs/AUFNAHMEN.md).
95
+
96
+ ## Bekannte Grenzen
97
+
98
+ - **Nur Mac.** Windows folgt.
99
+ - **Karten in Claude Desktop:** Claude Desktop zeigt die interaktiven Karten lokaler Erweiterungen derzeit nicht an. Schnippo fragt deshalb als Text im Chat.
100
+ - **Gemischte Framerates in der A-Roll:** Die Timeline übernimmt die Rate des Hauptmaterials, Takes mit anderer Rate sind nicht erprobt. B-Roll darf abweichen.
101
+ - **Lange Videos und kleine Claude-Abos:** Ein Interview mit gut 10 Minuten Sprache braucht etwa 9.200 Token für den ganzen Text, die Übersicht etwa 620. Hochgerechnet sind das bei einer Stunde Sprache rund 50.000 Token je Durchgang, dazu kommen Kontaktabzüge. Schnippo liest lange Takes deshalb seitenweise. Bei kleinen Abos kann die Nutzungsgrenze trotzdem mitten im Schnitt erreicht werden. Die Token sind gemessen, wie schnell ein bestimmtes Abo an seine Grenze kommt, ist nicht gemessen.
102
+ - **Noch nicht mit echtem Material erprobt:** Dreh nach Skript, Rohmaterial über 30 Minuten, sehr leise Aufnahmen. Hochformat-Quellen sind nur mit einem einzelnen Clip erprobt.
103
+ - **Seltener Resolve-Absturz:** Beim Einfügen von Grafiken ist Resolve 21.1.2 in Tests mehrfach abgestürzt. Die Ursache ist nicht geklärt. Die Reihenfolge beim Bau wurde angepasst, seitdem trat der Fehler in Tests nicht mehr auf. Speichere dein Projekt vor dem Bau.
104
+ - **Andere KI-Modelle:** Die Schnittqualität hängt vom Modell ab. Erprobt ist Schnippo nur mit Claude, das Modell braucht Bildverständnis.
105
+
106
+ ## Andere Programme
107
+
108
+ Für Claude Desktop ist die `.mcpb` der einfachste Weg. Andere MCP-fähige Programme starten Schnippo über [uv](https://docs.astral.sh/uv/) mit `uvx schnippo`. Du brauchst nur den Startbefehl und deinen `GLADIA_API_KEY`.
109
+
110
+ **Claude Code**
111
+
112
+ ```bash
113
+ claude mcp add schnippo -e GLADIA_API_KEY=dein-key -- uvx schnippo
114
+ ```
115
+
116
+ **Cursor** (`~/.cursor/mcp.json`)
117
+
118
+ ```json
119
+ {"mcpServers": {"schnippo": {"command": "uvx", "args": ["schnippo"], "env": {"GLADIA_API_KEY": "dein-key"}}}}
120
+ ```
121
+
122
+ **OpenAI Codex** (`~/.codex/config.toml`)
123
+
124
+ ```toml
125
+ [mcp_servers.schnippo]
126
+ command = "uvx"
127
+ args = ["schnippo"]
128
+ env = { GLADIA_API_KEY = "dein-key" }
129
+ ```
130
+
131
+ **VS Code mit Copilot** (`.vscode/mcp.json`)
132
+
133
+ ```json
134
+ {
135
+ "inputs": [{"type": "promptString", "id": "gladia", "description": "Gladia-Key", "password": true}],
136
+ "servers": {"schnippo": {"type": "stdio", "command": "uvx", "args": ["schnippo"], "env": {"GLADIA_API_KEY": "${input:gladia}"}}}
137
+ }
138
+ ```
139
+
140
+ Den Key kannst du stattdessen im macOS-Schlüsselbund ablegen: Sag im Chat „Speichere meinen Gladia-Key“, dann öffnet sich ein Eingabefenster und der Key läuft nicht durch den Chat. Plugin für Claude: [plugin/](plugin/).
141
+
142
+ ## FAQ
143
+
144
+ **Verändert Schnippo meine Dateien?** Nein. Timelines verweisen auf deine Originalclips, nichts wird neu gerendert oder gelöscht.
145
+
146
+ **Geht das mit der kostenlosen Resolve-Version?** Nein, sie unterstützt kein externes Scripting.
147
+
148
+ **Bleibt mein Key beim Update erhalten?** Ja, Claude Desktop speichert Key und Einstellungen pro Erweiterung, und die Kennung bleibt von Version zu Version gleich. Von der internen Vorversion „Resolve Cut“ musst du den Key einmal neu eintragen.
149
+
150
+ **Ich kannte Resolve Cut. Was passiert mit meinen Daten?** Schnippo übernimmt beim ersten Start Einstellungen, Glossare und Cache aus den alten Ordnern. Alte `resolve-cut-export/`-Ordner liest es weiter.
151
+
152
+ **Wie schalte ich den Update-Hinweis ab?** In den Einstellungen der Erweiterung, oder mit `SCHNIPPO_UPDATE_CHECK=0`.
153
+
154
+ ## Datenschutz
155
+
156
+ Keine Telemetrie, kein Schnippo-Server. Schnippo läuft auf deinem Rechner.
157
+
158
+ | Daten | Wohin | Wann |
159
+ |---|---|---|
160
+ | Originalmedien, Cache, Einstellungen | bleiben lokal | immer |
161
+ | Tonspur (16 kHz, mono, ohne Metadaten) | Gladia, `api.gladia.io` | beim Transkribieren |
162
+ | Transkripte, Schnittkontext, Einzelbilder und Kontaktabzüge | das KI-Modell deines Chats, zum Beispiel Claude | während des Schnitts |
163
+ | Suchbegriff oder Beschreibung | Freesound, ElevenLabs | nur mit eigenem Key und auf Wunsch |
164
+ | Keine Daten, nur die Abfrage der neuesten Version | GitHub, `api.github.com` | höchstens einmal am Tag, abschaltbar |
165
+
166
+ Nutze Schnippo nur mit Material, das du diesen Diensten geben darfst.
167
+
168
+ ### Privacy Policy
169
+
170
+ Vollständige Fassung: [docs/PRIVACY.md](docs/PRIVACY.md), online unter https://schnippo.com/privacy.
171
+
172
+ - **Datenerhebung:** Schnippo erhebt keine Daten und betreibt keinen eigenen Server.
173
+ - **Nutzung und Speicherung:** Transkripte, Analysen und Einstellungen liegen lokal in `~/Library/Caches/schnippo` und `~/.config/schnippo`.
174
+ - **Weitergabe an Dritte:** nur an die Dienste aus der Tabelle, mit deinem eigenen Konto.
175
+ - **Aufbewahrung:** Lokale Daten löschst du jederzeit mit `clear_cache` oder im Finder. Bei Gladia und deinem KI-Anbieter gelten deren Bedingungen.
176
+ - **Kontakt:** Seitz Dominik Visuals, https://sdv.design
177
+
178
+ ## Lizenzen
179
+
180
+ Schnippo steht unter der [MIT-Lizenz](LICENSE), © Seitz Dominik Visuals. Fremde Bestandteile: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
181
+
182
+ DaVinci Resolve ist eine Marke von Blackmagic Design. Schnippo ist ein unabhängiges Projekt und nicht mit Blackmagic Design verbunden.
183
+
184
+ ## Mitmachen
185
+
186
+ Fehler und Wünsche gern als [Issue](https://github.com/Seitzdominik/schnippo/issues/new/choose). Bitte hänge nie echtes Material oder Keys an.
187
+
188
+ ```bash
189
+ uv sync
190
+ uv run pytest
191
+ git config core.hooksPath .githooks
192
+ ```
193
+
194
+ Der Hook startet `tools/check_public.py`. Technische Erkenntnisse stehen in [docs/ENTWICKLUNG.md](docs/ENTWICKLUNG.md).
195
+
196
+ ---
197
+
198
+ ## English summary
199
+
200
+ **Schnippo** is an AI rough-cut assistant for DaVinci Resolve Studio. Tell Claude which folder to cut and get a non-destructive timeline built from your original clips, with optional B-roll, punch-ins, graphics, sound effects and reels. The interface and guides are in German; English footage works too.
201
+
202
+ **Requirements:** Apple Silicon Mac, DaVinci Resolve Studio 21.1+ with external scripting set to Local, Claude Desktop, your own Gladia key (roughly USD 0.61 per hour of audio).
203
+
204
+ **Install:** download the `.mcpb` from [Releases](https://github.com/Seitzdominik/schnippo/releases), double-click it, enter your key, then fully quit and reopen Claude from the menu bar. Say „Richte mich ein“ in a new chat. Other MCP clients can run `uvx schnippo` with `GLADIA_API_KEY` set.
205
+
206
+ **Privacy:** no telemetry and no Schnippo server. Audio goes to Gladia, transcripts and frames go to your AI model, and the update check asks GitHub once a day (optional). See [docs/PRIVACY.md](docs/PRIVACY.md).
207
+
208
+ **Licence:** MIT, © Seitz Dominik Visuals. DaVinci Resolve is a trademark of Blackmagic Design. This project is independent of Blackmagic Design.
@@ -0,0 +1,177 @@
1
+ # Schnippo
2
+
3
+ **KI-Rohschnitt für DaVinci Resolve.** Beta.
4
+
5
+ <!-- Bild oder GIF mit freigegebenem Material folgt: docs/bilder/schnippo.gif -->
6
+
7
+ ## Was ist Schnippo?
8
+
9
+ Schnippo baut per Chat aus deinen Takes einen Rohschnitt in DaVinci Resolve Studio: Du sagst Claude, welchen Ordner es schneiden soll, und bekommst eine fertige Timeline aus deinen Originalclips. Schnippo misst Sprache, Pausen, Versprecher und Ton, Claude trifft die redaktionellen Entscheidungen, und du behältst in Resolve das letzte Wort. Auf Wunsch kommen B-Roll, Zooms, Grafiken, Sound-Effekte und Reels aus dem langen Video dazu, alles nicht-destruktiv.
10
+
11
+ ## Voraussetzungen
12
+
13
+ - Mac mit Apple Silicon (Windows folgt)
14
+ - DaVinci Resolve Studio 21.1 oder neuer, unter Preferences → System → General „External scripting using“ auf **Local** (die kostenlose Version kann kein externes Scripting)
15
+ - Claude Desktop
16
+ - Eigener [Gladia-Key](https://www.gladia.io) für die Transkription
17
+
18
+ **Kosten:** Schnippo ist kostenlos. Gladia rechnet direkt mit dir ab, grob 0,61 USD je Stunde Audio. Vor jeder Transkription nennt Schnippo die Schätzung. Dazu kommt dein Claude-Abo.
19
+
20
+ ## Installation in 4 Schritten
21
+
22
+ 1. **Download:** Lade die neueste `schnippo-<version>.mcpb` von der [Releases-Seite](https://github.com/Seitzdominik/schnippo/releases).
23
+ 2. **Doppelklick:** Die Datei öffnet sich in Claude Desktop. Bestätige die Installation.
24
+ 3. **Key:** Trage deinen Gladia-Key in den Einstellungen der Erweiterung ein.
25
+ 4. **Claude vollständig neu starten:** Beende Claude über die Menüleiste (Claude → Beenden). Das Fenster zu schließen reicht nicht.
26
+
27
+ Neue Versionen installierst du genauso. Schnippo weist dich darauf hin und aktualisiert sich nie selbst.
28
+
29
+ ## Erster Schnitt
30
+
31
+ Starte einen neuen Chat und schreibe **„Richte mich ein“**. Das Onboarding prüft Resolve und den Key, fragt deine Vorlieben ab und legt auf Wunsch einen Projektordner an:
32
+
33
+ ```text
34
+ 2026-11-07_Mein_Film/
35
+ ├── A-Roll/ Takes mit Sprache
36
+ ├── B-Roll/ Bildmaterial ohne Sprache
37
+ ├── Bilder/ Standbilder, Logos
38
+ ├── Skript/ Skript, falls vorhanden
39
+ ├── brief.md Worum es geht, Format, Länge, Zielgruppe
40
+ └── glossar.txt Namen in richtiger Schreibweise
41
+ ```
42
+
43
+ Spickzettel für den Chat:
44
+
45
+ | Du schreibst | Schnippo macht |
46
+ |---|---|
47
+ | „Schneide Mein_Film“ | Rough Cut als neue Timeline, danach Rückfragen zu Grenzfällen |
48
+ | „Weiter“ | Nächster Durchgang: Fine Cut, B-Roll, Polish |
49
+ | „Mach aus dem Video 3 Reels“ | Hochformat-Reels aus den stärksten Stellen |
50
+ | „Füge dezente Sound-Effekte hinzu“ | Eigene Spur mit Effekten |
51
+ | „Hilfe“ | Überblick über alles Weitere |
52
+
53
+ Jeder Durchgang bekommt eine eigene Timeline. Schnippo überschreibt nichts und verändert deine Originaldateien nie. Alle Tools stehen in der [Referenz](docs/REFERENZ.md).
54
+
55
+ ## Aufnahme-Tipps
56
+
57
+ - **Für Reels in 4K filmen:** Dann bleibt beim Zuschnitt von 16:9 auf 9:16 genug Auflösung.
58
+ - **Versprecher:** Kurz stoppen, deutlich „nochmal“ sagen oder einmal klatschen und den ganzen Satz neu beginnen.
59
+ - **Bild:** Vor dem Sprechen 1 Sekunde ruhig in die Kamera schauen. Alle Takes mit derselben Framerate. B-Roll gern mit doppelter Rate, dann ist Zeitlupe möglich.
60
+ - **Ton:** Ruhiger Raum, 2–3 Sekunden Raumton am Anfang. Im Interview ein Mikro pro Person auf eigenem Kanal.
61
+ - **Namen:** Schreibweisen in die `glossar.txt`, dann stimmen sie schon im Transkript.
62
+
63
+ Mehr dazu in der [Referenz](docs/REFERENZ.md#so-nimmst-du-für-schnippo-auf), eigene Sound-Effekte aufnehmen: [docs/AUFNAHMEN.md](docs/AUFNAHMEN.md).
64
+
65
+ ## Bekannte Grenzen
66
+
67
+ - **Nur Mac.** Windows folgt.
68
+ - **Karten in Claude Desktop:** Claude Desktop zeigt die interaktiven Karten lokaler Erweiterungen derzeit nicht an. Schnippo fragt deshalb als Text im Chat.
69
+ - **Gemischte Framerates in der A-Roll:** Die Timeline übernimmt die Rate des Hauptmaterials, Takes mit anderer Rate sind nicht erprobt. B-Roll darf abweichen.
70
+ - **Lange Videos und kleine Claude-Abos:** Ein Interview mit gut 10 Minuten Sprache braucht etwa 9.200 Token für den ganzen Text, die Übersicht etwa 620. Hochgerechnet sind das bei einer Stunde Sprache rund 50.000 Token je Durchgang, dazu kommen Kontaktabzüge. Schnippo liest lange Takes deshalb seitenweise. Bei kleinen Abos kann die Nutzungsgrenze trotzdem mitten im Schnitt erreicht werden. Die Token sind gemessen, wie schnell ein bestimmtes Abo an seine Grenze kommt, ist nicht gemessen.
71
+ - **Noch nicht mit echtem Material erprobt:** Dreh nach Skript, Rohmaterial über 30 Minuten, sehr leise Aufnahmen. Hochformat-Quellen sind nur mit einem einzelnen Clip erprobt.
72
+ - **Seltener Resolve-Absturz:** Beim Einfügen von Grafiken ist Resolve 21.1.2 in Tests mehrfach abgestürzt. Die Ursache ist nicht geklärt. Die Reihenfolge beim Bau wurde angepasst, seitdem trat der Fehler in Tests nicht mehr auf. Speichere dein Projekt vor dem Bau.
73
+ - **Andere KI-Modelle:** Die Schnittqualität hängt vom Modell ab. Erprobt ist Schnippo nur mit Claude, das Modell braucht Bildverständnis.
74
+
75
+ ## Andere Programme
76
+
77
+ Für Claude Desktop ist die `.mcpb` der einfachste Weg. Andere MCP-fähige Programme starten Schnippo über [uv](https://docs.astral.sh/uv/) mit `uvx schnippo`. Du brauchst nur den Startbefehl und deinen `GLADIA_API_KEY`.
78
+
79
+ **Claude Code**
80
+
81
+ ```bash
82
+ claude mcp add schnippo -e GLADIA_API_KEY=dein-key -- uvx schnippo
83
+ ```
84
+
85
+ **Cursor** (`~/.cursor/mcp.json`)
86
+
87
+ ```json
88
+ {"mcpServers": {"schnippo": {"command": "uvx", "args": ["schnippo"], "env": {"GLADIA_API_KEY": "dein-key"}}}}
89
+ ```
90
+
91
+ **OpenAI Codex** (`~/.codex/config.toml`)
92
+
93
+ ```toml
94
+ [mcp_servers.schnippo]
95
+ command = "uvx"
96
+ args = ["schnippo"]
97
+ env = { GLADIA_API_KEY = "dein-key" }
98
+ ```
99
+
100
+ **VS Code mit Copilot** (`.vscode/mcp.json`)
101
+
102
+ ```json
103
+ {
104
+ "inputs": [{"type": "promptString", "id": "gladia", "description": "Gladia-Key", "password": true}],
105
+ "servers": {"schnippo": {"type": "stdio", "command": "uvx", "args": ["schnippo"], "env": {"GLADIA_API_KEY": "${input:gladia}"}}}
106
+ }
107
+ ```
108
+
109
+ Den Key kannst du stattdessen im macOS-Schlüsselbund ablegen: Sag im Chat „Speichere meinen Gladia-Key“, dann öffnet sich ein Eingabefenster und der Key läuft nicht durch den Chat. Plugin für Claude: [plugin/](plugin/).
110
+
111
+ ## FAQ
112
+
113
+ **Verändert Schnippo meine Dateien?** Nein. Timelines verweisen auf deine Originalclips, nichts wird neu gerendert oder gelöscht.
114
+
115
+ **Geht das mit der kostenlosen Resolve-Version?** Nein, sie unterstützt kein externes Scripting.
116
+
117
+ **Bleibt mein Key beim Update erhalten?** Ja, Claude Desktop speichert Key und Einstellungen pro Erweiterung, und die Kennung bleibt von Version zu Version gleich. Von der internen Vorversion „Resolve Cut“ musst du den Key einmal neu eintragen.
118
+
119
+ **Ich kannte Resolve Cut. Was passiert mit meinen Daten?** Schnippo übernimmt beim ersten Start Einstellungen, Glossare und Cache aus den alten Ordnern. Alte `resolve-cut-export/`-Ordner liest es weiter.
120
+
121
+ **Wie schalte ich den Update-Hinweis ab?** In den Einstellungen der Erweiterung, oder mit `SCHNIPPO_UPDATE_CHECK=0`.
122
+
123
+ ## Datenschutz
124
+
125
+ Keine Telemetrie, kein Schnippo-Server. Schnippo läuft auf deinem Rechner.
126
+
127
+ | Daten | Wohin | Wann |
128
+ |---|---|---|
129
+ | Originalmedien, Cache, Einstellungen | bleiben lokal | immer |
130
+ | Tonspur (16 kHz, mono, ohne Metadaten) | Gladia, `api.gladia.io` | beim Transkribieren |
131
+ | Transkripte, Schnittkontext, Einzelbilder und Kontaktabzüge | das KI-Modell deines Chats, zum Beispiel Claude | während des Schnitts |
132
+ | Suchbegriff oder Beschreibung | Freesound, ElevenLabs | nur mit eigenem Key und auf Wunsch |
133
+ | Keine Daten, nur die Abfrage der neuesten Version | GitHub, `api.github.com` | höchstens einmal am Tag, abschaltbar |
134
+
135
+ Nutze Schnippo nur mit Material, das du diesen Diensten geben darfst.
136
+
137
+ ### Privacy Policy
138
+
139
+ Vollständige Fassung: [docs/PRIVACY.md](docs/PRIVACY.md), online unter https://schnippo.com/privacy.
140
+
141
+ - **Datenerhebung:** Schnippo erhebt keine Daten und betreibt keinen eigenen Server.
142
+ - **Nutzung und Speicherung:** Transkripte, Analysen und Einstellungen liegen lokal in `~/Library/Caches/schnippo` und `~/.config/schnippo`.
143
+ - **Weitergabe an Dritte:** nur an die Dienste aus der Tabelle, mit deinem eigenen Konto.
144
+ - **Aufbewahrung:** Lokale Daten löschst du jederzeit mit `clear_cache` oder im Finder. Bei Gladia und deinem KI-Anbieter gelten deren Bedingungen.
145
+ - **Kontakt:** Seitz Dominik Visuals, https://sdv.design
146
+
147
+ ## Lizenzen
148
+
149
+ Schnippo steht unter der [MIT-Lizenz](LICENSE), © Seitz Dominik Visuals. Fremde Bestandteile: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
150
+
151
+ DaVinci Resolve ist eine Marke von Blackmagic Design. Schnippo ist ein unabhängiges Projekt und nicht mit Blackmagic Design verbunden.
152
+
153
+ ## Mitmachen
154
+
155
+ Fehler und Wünsche gern als [Issue](https://github.com/Seitzdominik/schnippo/issues/new/choose). Bitte hänge nie echtes Material oder Keys an.
156
+
157
+ ```bash
158
+ uv sync
159
+ uv run pytest
160
+ git config core.hooksPath .githooks
161
+ ```
162
+
163
+ Der Hook startet `tools/check_public.py`. Technische Erkenntnisse stehen in [docs/ENTWICKLUNG.md](docs/ENTWICKLUNG.md).
164
+
165
+ ---
166
+
167
+ ## English summary
168
+
169
+ **Schnippo** is an AI rough-cut assistant for DaVinci Resolve Studio. Tell Claude which folder to cut and get a non-destructive timeline built from your original clips, with optional B-roll, punch-ins, graphics, sound effects and reels. The interface and guides are in German; English footage works too.
170
+
171
+ **Requirements:** Apple Silicon Mac, DaVinci Resolve Studio 21.1+ with external scripting set to Local, Claude Desktop, your own Gladia key (roughly USD 0.61 per hour of audio).
172
+
173
+ **Install:** download the `.mcpb` from [Releases](https://github.com/Seitzdominik/schnippo/releases), double-click it, enter your key, then fully quit and reopen Claude from the menu bar. Say „Richte mich ein“ in a new chat. Other MCP clients can run `uvx schnippo` with `GLADIA_API_KEY` set.
174
+
175
+ **Privacy:** no telemetry and no Schnippo server. Audio goes to Gladia, transcripts and frames go to your AI model, and the update check asks GitHub once a day (optional). See [docs/PRIVACY.md](docs/PRIVACY.md).
176
+
177
+ **Licence:** MIT, © Seitz Dominik Visuals. DaVinci Resolve is a trademark of Blackmagic Design. This project is independent of Blackmagic Design.
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "schnippo"
3
+ version = "0.11.0"
4
+ description = "KI-Rohschnitt für DaVinci Resolve: MCP-Server für Rohschnitte per Chat"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ keywords = [
8
+ "mcp",
9
+ "davinci resolve",
10
+ "video editing",
11
+ "rough cut",
12
+ "transcript",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: MacOS X",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "Natural Language :: German",
19
+ "Operating System :: MacOS",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Multimedia :: Video :: Non-Linear Editor",
22
+ ]
23
+ requires-python = ">=3.14,<3.16"
24
+ dependencies = [
25
+ "httpx>=0.28.1",
26
+ "mcp[cli]>=2.3.0",
27
+ "numpy>=2.5.4",
28
+ "opencv-python-headless>=4.10,<5",
29
+ "pillow>=13.0.0",
30
+ "portable-ffmpeg-full==0.5.0",
31
+ "pydantic>=2.14.2",
32
+ "python-docx>=1.2.0",
33
+ "rapidfuzz>=3.14.6",
34
+ ]
35
+
36
+ [[project.authors]]
37
+ name = "Seitz Dominik Visuals"
38
+
39
+ [project.urls]
40
+ Homepage = "https://schnippo.com"
41
+ Repository = "https://github.com/Seitzdominik/schnippo"
42
+ Issues = "https://github.com/Seitzdominik/schnippo/issues"
43
+
44
+ [project.optional-dependencies]
45
+ motion = ["playwright==1.64.1"]
46
+
47
+ [project.scripts]
48
+ schnippo = "schnippo.server:main"
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.13.7,<0.14.0"]
52
+ build-backend = "uv_build"
53
+
54
+ [dependency-groups]
55
+ dev = [
56
+ "playwright==1.64.1",
57
+ "pytest>=9.2.0",
58
+ ]
59
+
60
+ [tool.pytest.ini_options]
61
+ testpaths = ["tests"]
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "schnippo"
3
+ version = "0.11.0"
4
+ description = "KI-Rohschnitt für DaVinci Resolve: MCP-Server für Rohschnitte per Chat"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ authors = [
8
+ { name = "Seitz Dominik Visuals" }
9
+ ]
10
+ keywords = ["mcp", "davinci resolve", "video editing", "rough cut", "transcript"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: MacOS X",
14
+ "Intended Audience :: End Users/Desktop",
15
+ "Natural Language :: German",
16
+ "Operating System :: MacOS",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: Multimedia :: Video :: Non-Linear Editor",
19
+ ]
20
+ requires-python = ">=3.14,<3.16" # tested with Resolve 21.1.2 fusionscript: 3.14 (default via .python-version) and 3.15
21
+ dependencies = [
22
+ "httpx>=0.28.1",
23
+ "mcp[cli]>=2.3.0",
24
+ "numpy>=2.5.4",
25
+ "opencv-python-headless>=4.10,<5",
26
+ "pillow>=13.0.0",
27
+ "portable-ffmpeg-full==0.5.0",
28
+ "pydantic>=2.14.2",
29
+ "python-docx>=1.2.0",
30
+ "rapidfuzz>=3.14.6",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://schnippo.com"
35
+ Repository = "https://github.com/Seitzdominik/schnippo"
36
+ Issues = "https://github.com/Seitzdominik/schnippo/issues"
37
+
38
+ [project.optional-dependencies]
39
+ # installed on first use of motion graphics (motion_setup), not with the extension
40
+ motion = ["playwright==1.64.1"]
41
+
42
+ [project.scripts]
43
+ schnippo = "schnippo.server:main"
44
+
45
+ [build-system]
46
+ requires = ["uv_build>=0.13.7,<0.14.0"]
47
+ build-backend = "uv_build"
48
+
49
+ [dependency-groups]
50
+ dev = [
51
+ "playwright==1.64.1",
52
+ "pytest>=9.2.0",
53
+ ]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """schnippo: AI rough-cut MCP server for DaVinci Resolve Studio."""
2
+
3
+ __version__ = "0.11.0"
@@ -0,0 +1,166 @@
1
+ """Acoustic analysis of the 16 kHz mono WAV (numpy, no Torch).
2
+
3
+ * RMS envelope (25 ms window, 10 ms hop) in dBFS.
4
+ * Noise floor = low percentile of the envelope; activity threshold = floor + delta.
5
+ This replaces the Phase-1 rule max(-35 dB, mean-18 dB), which on a quiet recording with a very low
6
+ room noise put the threshold *above* half of the speech frames.
7
+ * Voicing per frame = peak of the normalised autocorrelation in the 75–400 Hz lag range (40 ms window).
8
+ Voiced sounds ("äh", "hm", vowels) have a clear periodicity, breaths/clicks do not.
9
+
10
+ Regions of activity are classified as ``voiced`` (filler candidate), ``breath`` (unvoiced, breath-like
11
+ duration) or ``click`` (very short transient).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import wave
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+
22
+ SR = 16000
23
+ HOP = 160 # 10 ms
24
+ WIN = 400 # 25 ms
25
+ VWIN = 640 # 40 ms voicing window
26
+ LAG_MIN, LAG_MAX = SR // 400, SR // 75 # 40 .. 213 samples
27
+
28
+
29
+ @dataclass
30
+ class AcousticParams:
31
+ floor_percentile: float = 10.0
32
+ delta_db: float = 25.0 # activity threshold above the noise floor
33
+ min_threshold_db: float = -60.0 # never below this (digital silence / heavy denoise)
34
+ max_threshold_db: float = -30.0
35
+ min_region_s: float = 0.06
36
+ merge_gap_s: float = 0.08
37
+ voiced_corr: float = 0.6 # frame is voiced if autocorrelation peak >= this
38
+ voiced_min_s: float = 0.15 # region needs at least this much voiced material (calibrated on real take)
39
+ voiced_ratio: float = 0.6 # ... and this share of its active frames voiced
40
+ click_max_s: float = 0.08
41
+
42
+ def key(self) -> list:
43
+ return [self.floor_percentile, self.delta_db, self.min_threshold_db, self.max_threshold_db, self.min_region_s,
44
+ self.merge_gap_s, self.voiced_corr, self.voiced_min_s, self.voiced_ratio, self.click_max_s, 2]
45
+
46
+
47
+ @dataclass
48
+ class Acoustic:
49
+ env_db: np.ndarray # per 10 ms frame
50
+ voicing: np.ndarray # per 10 ms frame, 0..1
51
+ floor_db: float
52
+ threshold_db: float
53
+ duration_s: float
54
+ params: AcousticParams = field(default_factory=AcousticParams)
55
+
56
+ # ---- frame helpers ----
57
+ def _sl(self, t0: float, t1: float) -> slice:
58
+ return slice(max(0, int(round(t0 * 100))), max(0, int(round(t1 * 100))))
59
+
60
+ def active(self, t0: float, t1: float) -> np.ndarray:
61
+ return self.env_db[self._sl(t0, t1)] > self.threshold_db
62
+
63
+ def regions(self, t0: float, t1: float) -> list[tuple[float, float]]:
64
+ """Active (non-silent) sub-intervals of [t0, t1]."""
65
+ act = self.active(t0, t1)
66
+ base = int(round(t0 * 100))
67
+ out: list[list[float]] = []
68
+ i = 0
69
+ while i < len(act):
70
+ if act[i]:
71
+ j = i
72
+ while j < len(act) and act[j]:
73
+ j += 1
74
+ a, b = (base + i) / 100, (base + j) / 100
75
+ if out and a - out[-1][1] <= self.params.merge_gap_s:
76
+ out[-1][1] = b
77
+ else:
78
+ out.append([a, b])
79
+ i = j
80
+ else:
81
+ i += 1
82
+ return [(max(a, t0), min(b, t1)) for a, b in out if b - a >= self.params.min_region_s]
83
+
84
+ def classify(self, t0: float, t1: float) -> dict:
85
+ sl = self._sl(t0, t1)
86
+ env, voi = self.env_db[sl], self.voicing[sl]
87
+ act = env > self.threshold_db
88
+ voiced = (voi >= self.params.voiced_corr) & act
89
+ vs = float(voiced.sum()) / 100
90
+ ratio = float(voiced.sum()) / max(1, int(act.sum()))
91
+ dur = t1 - t0
92
+ if vs >= self.params.voiced_min_s and ratio >= self.params.voiced_ratio:
93
+ cls = "voiced"
94
+ elif dur <= self.params.click_max_s:
95
+ cls = "click"
96
+ else:
97
+ cls = "breath"
98
+ return {"class": cls, "peak_db": round(float(env.max()) if len(env) else -120.0, 1),
99
+ "voiced_s": round(vs, 2), "voiced_ratio": round(ratio, 2)}
100
+
101
+ def silent_runs(self, t0: float, t1: float, min_s: float) -> list[tuple[float, float]]:
102
+ act = self.active(t0, t1)
103
+ base = int(round(t0 * 100))
104
+ out, i = [], 0
105
+ while i < len(act):
106
+ if not act[i]:
107
+ j = i
108
+ while j < len(act) and not act[j]:
109
+ j += 1
110
+ if (j - i) / 100 >= min_s:
111
+ out.append(((base + i) / 100, (base + j) / 100))
112
+ i = j
113
+ else:
114
+ i += 1
115
+ return out
116
+
117
+ # ---- persistence ----
118
+ def save(self, path: Path) -> None:
119
+ np.savez_compressed(path, env=self.env_db.astype(np.float32), voi=self.voicing.astype(np.float16),
120
+ meta=np.array([self.floor_db, self.threshold_db, self.duration_s]),
121
+ key=np.array(self.params.key(), dtype=float))
122
+
123
+ @classmethod
124
+ def load(cls, path: Path, params: AcousticParams) -> "Acoustic | None":
125
+ if not path.exists():
126
+ return None
127
+ d = np.load(path)
128
+ if list(d["key"]) != [float(x) for x in params.key()]:
129
+ return None
130
+ floor, thr, dur = (float(x) for x in d["meta"])
131
+ return cls(d["env"].astype(np.float64), d["voi"].astype(np.float64), floor, thr, dur, params)
132
+
133
+
134
+ def read_wav(path: Path) -> np.ndarray:
135
+ with wave.open(str(path)) as w:
136
+ if w.getframerate() != SR or w.getnchannels() != 1 or w.getsampwidth() != 2:
137
+ raise ValueError("expected 16 kHz mono 16-bit WAV")
138
+ return np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float64) / 32768.0
139
+
140
+
141
+ def _frames(xp: np.ndarray, win: int, i0: int, i1: int) -> np.ndarray:
142
+ """Frames i0..i1 (hop 10 ms) of the padded signal xp (padded by win//2 in front)."""
143
+ idx = np.arange(win)[None, :] + (np.arange(i0, i1) * HOP)[:, None]
144
+ return xp[idx]
145
+
146
+
147
+ def analyze_wav(path: Path, params: AcousticParams | None = None) -> Acoustic:
148
+ params = params or AcousticParams()
149
+ x = read_wav(path)
150
+ n = 1 + max(0, (len(x) - 1) // HOP)
151
+ xp_e = np.pad(x, (WIN // 2, WIN))
152
+ xp_v = np.pad(x, (VWIN // 2, VWIN))
153
+ env = np.empty(n)
154
+ voi = np.empty(n)
155
+ nfft, hann = 1024, np.hanning(VWIN)[None, :]
156
+ for i0 in range(0, n, 4096): # chunked: bounded memory for hour-long material
157
+ i1 = min(n, i0 + 4096)
158
+ fe = _frames(xp_e, WIN, i0, i1)
159
+ env[i0:i1] = 10 * np.log10(np.mean(fe * fe, axis=1) + 1e-12)
160
+ fv = _frames(xp_v, VWIN, i0, i1)
161
+ fv = (fv - fv.mean(axis=1, keepdims=True)) * hann
162
+ ac = np.fft.irfft(np.abs(np.fft.rfft(fv, nfft)) ** 2, nfft)[:, :LAG_MAX + 1]
163
+ voi[i0:i1] = np.clip(ac[:, LAG_MIN:LAG_MAX + 1].max(axis=1) / (ac[:, 0] + 1e-12), 0, 1)
164
+ floor = float(np.percentile(env, params.floor_percentile))
165
+ thr = float(np.clip(floor + params.delta_db, params.min_threshold_db, params.max_threshold_db))
166
+ return Acoustic(env, voi, floor, thr, len(x) / SR, params)