kimal3 0.4.0 → 0.5.0

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.
@@ -0,0 +1,302 @@
1
+ ---
2
+ description: Show a task instead of describing it — I record it and turn it into a plan
3
+ ---
4
+
5
+ <!-- kimal3:erzeugt — this file is maintained by `kimal3 skill install` and
6
+ overwritten on every update. Your own changes will be lost; if you want
7
+ to keep them, copy the file under a different name. -->
8
+ # Show
9
+
10
+ > You do a task once on screen and say out loud what you are doing and why.
11
+ > I record it, evaluate it on your computer and write a description of it into the
12
+ > project — so that we can automate the task afterwards without you first having to write
13
+ > it down as a requirement.
14
+
15
+ Carry out the steps in order. Stay in the coaching tone: English, friendly, **one question at a
16
+ time and always with a suggestion**, the why before the how, no prior knowledge assumed. The
17
+ mechanics are done by the program (`kimal3 show …`); you lead the conversation with the person. How
18
+ to deal with `kimal3`'s exit codes in general is in `~/.claude/skills/kimal3/SKILL.md`; here are only
19
+ the cases of this command.
20
+
21
+ ## 🔴 Three rules for the whole conversation
22
+
23
+ 1. **Before the yes in step 10 you read nothing from the recording.** No `zeitleiste.md`, no
24
+ picture, no log, no other file from `material/aufnahmen/` — and you don't **search** there
25
+ either: no grep, no glob, no file search across the project that includes this folder. What
26
+ lies there only leaves the computer with the person's yes. You may read the output of
27
+ `kimal3 show …` — it contains only numbers, times and folder names.
28
+ 2. **Install nothing, record nothing, delete nothing without an explicit yes to exactly that
29
+ step.** You open a file from the recording for the person only in Notepad, and in a way that
30
+ your call returns at once: in Bash `cmd //c start "" notepad "<path>"`, in PowerShell
31
+ `Start-Process notepad -ArgumentList '"<path>"'`. A bare `notepad "<path>"` blocks until the
32
+ window is closed again. Never in an IDE or an editor that Claude Code is attached to (selected
33
+ lines end up in the conversation there by themselves).
34
+ 3. **A failure never looks like a success.** If a `kimal3 show …` doesn't end with `0`, say
35
+ honestly what its message says, and don't carry on as if it had worked.
36
+
37
+ Every `kimal3 show …` runs **in the project folder** (step 3) — the program finds the recording
38
+ through the folder it is started in. If a recording lies in another project, change into it first:
39
+ the project folder is the part of the path before `material/aufnahmen/`.
40
+
41
+ ## 1. Arrive and look
42
+
43
+ First run `kimal3 show status`. It only reads. **Don't** read the “Set up:” block out to the
44
+ person — just note whether anything in it is missing (a line with “missing”, “can't be checked”,
45
+ “I can't tell” or “(… is needed)” → step 4).
46
+
47
+ | The output says | What you do |
48
+ |---|---|
49
+ | `kimal3 show` is Windows-only for now; the Mac follows. | Say exactly that, kindly: showing a task doesn't work on this computer yet, the Mac is coming. Offer that the person describes the task in words instead. The command ends here. |
50
+ | “I can't tell whether a recording is running.” (exit code 1) | Say honestly that you can't tell right now. **Start nothing.** Offer to look again in a minute. |
51
+ | “Running since 09:12 (3 minutes), folder `…`, stops at 09:27 at the latest.” | A recording from an earlier session is still running. If the folder is a full path outside this project, first change into its project folder. Then ask: “There's still a recording running from 09:12 — are you through with showing the task? Then I'll stop it now.” Yes → step 8. |
52
+ | “Running (started in another session).” | A recording is running that this project can't match. Say so, **don't start a second one**, and suggest checking with `kimal3 show status` once it has ended. |
53
+ | “No recording is running.” followed by “The last one (`…`) stopped by itself after 15 minutes.” | A recording is finished but probably not evaluated yet. **Don't silently start a new one over it.** Ask with a suggestion: “The recording from 09:12 stopped by itself after 15 minutes — shall I evaluate it, or would you rather show the task again? I'd evaluate it; whatever you did after the 15 minutes is then missing.” Evaluate → step 9 (the time is in the folder name, `…-0912`). Again → carry on here. |
54
+ | “No recording is running.” followed by “The last one (`…`) broke off.” | Kindly: the last recording broke off in the middle — the person did nothing wrong. Offer to show the task again. |
55
+ | “No recording is running.” | Carry on. |
56
+
57
+ The last line says whether the folder is right — remember it for step 3:
58
+ “Recordings go here: …” (right) · “Switch into a project first …” (the session is in the
59
+ workbench) · “The project folder is nested too deeply for the recorder …” (the path is too long).
60
+
61
+ If the session is in the workbench, say **that first**, in one sentence: the recording belongs in
62
+ a project, and you'll suggest one in a moment. Then explain in at most five short sentences what
63
+ happens, and ask **one** question:
64
+
65
+ > “You show me the task once and talk while you do it. I record the screen, sound, clicks and
66
+ > keys. It stays on your computer. Before I read any of it, I'll ask you. Which task do you want
67
+ > to show me — in one sentence?”
68
+
69
+ Until the person has answered, nothing is installed and nothing is recorded.
70
+
71
+ ## 2. The task in one sentence
72
+
73
+ Wait for the answer. You take the person's sentence **unchanged** — it will stand at the top of
74
+ the timeline, and exactly where it and the demonstration differ lies the most important follow-up
75
+ question (step 11). A rough sentence is enough; don't dig.
76
+
77
+ ## 3. The right folder
78
+
79
+ - **Recordings already go to the right place here?** Carry on with step 4.
80
+ - **The session is in the workbench** (`workbench-…`): suggest a project with a short name from the
81
+ task — lower case, hyphens: “I'd create the project `projects/quote-list` for this — does that
82
+ work?” (if the folder in the workbench is called `projekte/`, then there). Only after the yes do
83
+ you create **only** `plan/` and `material/` in it, nothing in advance. From now on every
84
+ `kimal3 show …` runs in this project folder (e.g. `cd "<project folder>" && kimal3 show status`).
85
+ Check with `kimal3 show status` that the last line now says “Recordings go here: …”.
86
+ - **The path is too deep:** name the number from the message and why (the recorder fails on long
87
+ paths). Suggest creating the project closer to the drive, and ask before you create anything.
88
+
89
+ ## 4. Setting up — only if something is missing
90
+
91
+ If nothing is missing, skip this step without a word about it. If something is missing, say
92
+ **exactly this first** and wait for the answer:
93
+
94
+ > “For this I'll set up the recorder once — a free program that records screen, sound, clicks
95
+ > and keys. About 2.1 GB, 10 to 20 minutes depending on your connection, only in your user
96
+ > folder, no admin rights. Because it records keystrokes, your company's antivirus may flag it;
97
+ > on a work computer, check with IT first if in doubt. Shall I?”
98
+
99
+ - **Yes:** start `kimal3 show setup` **in the background** — it can take longer than the ten
100
+ minutes a command may run in the foreground. Say that it is running and the person can do
101
+ something else meanwhile. **Wait for the notice that it has finished** — don't check in between,
102
+ and don't sleep in a loop. After that:
103
+ - exit code `0` → `kimal3 show status`; if all four parts are there (uv, recorder, FFmpeg,
104
+ speech model), carry on with step 5.
105
+ - exit code `1` (“Step … didn't work.”) → name that step, and offer a ready-made
106
+ message to IT: **what** (uv from Astral · the recorder openadapt-capture 1.3.0, MIT licence,
107
+ with Python 3.12 · FFmpeg · the speech model large-v3-turbo from Hugging Face), **what for**
108
+ (showing a work task once on screen and evaluating it on the computer), **where from**
109
+ (astral.sh or winget, pypi.org, huggingface.co), **which folders** (only the user folder, no
110
+ admin rights).
111
+ - **No:** “No problem — you can do it any time later with `/k3show`.” The command ends here.
112
+
113
+ ## 5. The briefing before the start
114
+
115
+ Say these four points before anything records:
116
+
117
+ 1. **All screens** are recorded. Close anything that shows other people's data, switch on “Do
118
+ not disturb”, and not during a call. If you can, use a **test customer** instead of a real
119
+ one.
120
+ 2. **Don't type a password** — every key is recorded.
121
+ 3. Say out loud **why** you do something.
122
+ 4. When you're done, come back here and type **“done”**; the recording stops by itself after 15
123
+ minutes at the latest.
124
+
125
+ Then: “Type ‘go’ when you're ready.” — and wait.
126
+
127
+ - **A clear go** — “go”, “ready”, “start” — → step 6.
128
+ - **A bare “ok” or “yes”** → ask once: “Shall I start now?”
129
+ - **Anything else** → answer and say that it starts with “go”.
130
+
131
+ **Nothing starts before the go.**
132
+
133
+ ## 6. Starting
134
+
135
+ After the go: `kimal3 show start '<the person's sentence>'`. Pass the sentence as **one** argument,
136
+ so that the shell changes nothing in it — in Bash in single quotes (a `'` inside as `'\''`), in
137
+ PowerShell in single quotes (a `'` inside doubled). `--at-most` (1 to 15 minutes) only if the person
138
+ explicitly wants it shorter.
139
+
140
+ | The output says | What you do |
141
+ |---|---|
142
+ | “The recording is running (folder …), stops at … at the latest.” (exit code 0) | Say exactly: “Running. Show me — and type ‘done’ here when you're through.” **Then end your turn** — no further command, no checking. |
143
+ | “A recording is already running (since 09:12, folder `…`).” · “A recording is already starting …” | No second one is created. Ask: “There's still a recording running from 09:12 — shall I stop it so that we can start again?” Yes → `kimal3 show stop` (step 8), then back to step 5. |
144
+ | “A recording is already running (started in another session).” | Ask: “There's still a recording running from another session — shall I stop it so that we can start again?” Yes → `kimal3 show stop`; if it says it can't match it here, say so honestly and suggest waiting until it stops by itself (`kimal3 show status`). |
145
+ | “I can't tell whether a recording is running — so I won't start a second one.” | Say it honestly, force nothing, look again later. |
146
+ | “The recorder isn't set up yet: `kimal3 show setup`.” | Back to step 4. |
147
+ | “The recording didn't start: <reason>. Log: …” | Name the reason. **Don't open the log** — it contains window titles. If “Whether the recorder is still running: `kimal3 show status`.” follows, run `kimal3 show status` and say what it reports. Offer another attempt. |
148
+ | “Switch into a project first …” · “The project folder is nested too deeply …” | Back to step 3. |
149
+
150
+ ## 7. While recording
151
+
152
+ If the person writes anything other than “done” (a question, a thought), answer briefly and end
153
+ with: “The recording is still running — type ‘done’ when you're through.” Open nothing on the
154
+ screen meanwhile (it would end up in the video) and don't stop the recording on your own.
155
+ “done”, “finished”, “I'm through” and the like → step 8.
156
+
157
+ ## 8. Stopping
158
+
159
+ First say: “I'm finishing the recording — that can take a minute, with a long recording several.”
160
+ Otherwise the person sees nothing happen while it finishes.
161
+
162
+ Then `kimal3 show stop` — with the **longest wait the tool allows (10 minutes)**: the recorder finishes
163
+ the video, and with a long recording that takes a while; with the default of 2 minutes the call
164
+ would break off in the middle. The program answers after a little over 8 minutes at the latest, so
165
+ before the 10 minutes are up.
166
+
167
+ | The output says | What you do |
168
+ |---|---|
169
+ | “Recording stopped: 1:33 minutes, 30 clicks, 49 keys, audio present.” | Repeat the numbers in one sentence, carry on with step 9. |
170
+ | “The recording is still being finished — run `kimal3 show stop` again in a moment.” (exit code 1) | Say: “The recorder is still finishing the recording — I'll check again in a moment.” Then run `kimal3 show stop` again, again with the 10-minute wait. **Never** say “not closed cleanly”, and don't offer a new recording. |
171
+ | “The recording stopped by itself after 15 minutes: …” (exit code 0) | Say: “The recording stopped by itself after 15 minutes — whatever you did after that isn't in it.” Carry on with step 9. |
172
+ | “No recording is running.” | Check with `kimal3 show status`. If it says “The last one (`…`) stopped by itself after … minutes.”, carry on with step 9; otherwise say honestly that there is no recording. |
173
+ | “I can't tell whether a recording is running.” | Say it honestly — **never** “stopped”. Offer to look again in a minute with `kimal3 show status`. |
174
+ | “The recording wasn't closed cleanly: <reason>.” (exit code 1) | Name the reason and offer a new recording. |
175
+ | “The recording broke off: <reason>. …” (exit code 1) | Kindly: the person did nothing wrong. Name the reason and offer a new recording. |
176
+ | “The recording couldn't be stopped …” · “A recording is running that I can't match here …” | Run `kimal3 show status` and say honestly what it reports — **never** “stopped”. If the session is in the wrong folder, change into the recording's project. |
177
+
178
+ ## 9. Evaluating — on the person's computer
179
+
180
+ `kimal3 show evaluate` (without a folder it takes the project's latest recording; the language of
181
+ the speech recognition follows the account — `--language de` or `--language en` only if the person
182
+ spoke the other language). This takes about as long as the recording:
183
+
184
+ - **Recording up to about 3 minutes:** in the foreground, with the longest wait (10 minutes).
185
+ - **Longer — and always when it stopped by itself after 15 minutes:** **in the background**, as
186
+ with the setup. Say that it takes about as long as the recording, and **wait for the notice that
187
+ it has finished** — no checking in between, no loop.
188
+
189
+ | The output says | What you do |
190
+ |---|---|
191
+ | “Timeline ready: … File: `…`.” | Say the numbers in one sentence. Remember the path — **don't open the file**. Carry on with step 10. |
192
+ | “I didn't recognise any speech — was the microphone muted or the wrong one selected?” | Ask exactly that and offer a new recording. |
193
+ | “The recording has no audio track — without sound there's no timeline. A new recording helps.” | Offer a new recording. |
194
+ | “I can't find a recording in this project (`material/aufnahmen/`).” | The session isn't in the recording's project: change into it (the part of the path before `material/aufnahmen/`, from `kimal3 show status` or the stop message) and evaluate again. |
195
+ | “The recording folder doesn't exist: …” | Say honestly that the recording isn't (or is no longer) there. Check with `kimal3 show status`; otherwise offer a new recording. |
196
+ | “The recorder isn't set up yet: `kimal3 show setup`.” | Back to step 4, then evaluate again. |
197
+ | “The timeline wasn't created: <reason>.” | Name the reason; if the speech model is missing, back to step 4. |
198
+ | “The recording is still running — first `kimal3 show stop`.” | Back to step 8. |
199
+
200
+ ## 10. Data protection — before you read
201
+
202
+ First one question, and wait for the answer:
203
+
204
+ > “Did the demonstration show real data about people — customer names, email addresses?”
205
+
206
+ **Yes:** then the four questions from `context/security.md` (in the workplace above the workbench;
207
+ in a German workplace `kontext/sicherheit.md`) come **now**, before reading — one at a time, each
208
+ with a suggestion: which fields · where to (to Anthropic, the company behind Claude, data centres
209
+ in the USA) · on what legal basis · can it be done more sparingly, e.g. once more with test
210
+ customers. You write the answers to `results/data-protection-demonstration.md` (in a German
211
+ workbench `ergebnisse/`). If the answer is “once more with test customers”, read nothing, suggest
212
+ deleting this recording (step 13, the sentence **without** the plan), and start again at step 5.
213
+
214
+ Then this question, word for word:
215
+
216
+ > “For me to read the timeline, it goes to Anthropic: what you said, where you clicked (with the
217
+ > names of the buttons and fields — these can contain names) and what you typed. Screenshots show
218
+ > your whole screen — I'll only look at those where the text isn't enough, and ask you before
219
+ > each one. Shall I open the file for you so you can look first? Agreed — or would you rather
220
+ > leave out the typed parts?”
221
+
222
+ - **“Open it”:** in Notepad, as in rule 2 (`cmd //c start "" notepad "<full path>"` or
223
+ `Start-Process`), with the full path to `zeitleiste.md` (the project folder plus the path from
224
+ step 9). The person reads it themselves; you don't read along. Then ask the question about
225
+ agreeing once more.
226
+ - **“Without the typed parts”:** `kimal3 show evaluate --without-typed` — with the same
227
+ `--language` as the first time, if you set one, and with the same waiting rule as in step 9. The
228
+ timeline is rewritten, and typed text then stands as “[typed, left out]”. The person has agreed
229
+ to this version → step 11.
230
+ - **Agreed:** → step 11.
231
+ - **No:** read nothing. Say: “You can read the file yourself and describe the task to me — or I
232
+ delete the recording.”
233
+ - **The person describes it:** take the description, ask follow-up questions as in step 11,
234
+ point 4 (one at a time), and carry on with step 12 — plan and way to build it **from the
235
+ description**, without reading anything from the recording. Then step 13.
236
+ - **Delete:** step 13, the sentence **without** the plan.
237
+
238
+ ## 11. Read, retell, ask
239
+
240
+ Now — and only now — you read `zeitleiste.md`, and only this file.
241
+
242
+ 1. **Retell the task:** in numbered steps, in the **person's words** (quote what was said), each
243
+ step with the time from the timeline (`0:26`).
244
+ 2. **Look for the contradictions, explicitly.** Three sources say something about the task: the
245
+ task sentence at the top of the timeline, what the person **says**, and what they **click and
246
+ type**. Go through the task sentence part by part (what, where to, to whom, when) and ask for
247
+ each part: does the demonstration show exactly that? Then check every spoken sentence against
248
+ the click next to it. Every place where two sources differ, you name in the retelling with ⚠️ —
249
+ for example: “You clicked ‘tomorrow’, but said ‘for the current day’ — which one applies?” or
250
+ “The task sentence says ‘into a channel’, but you showed a direct message — which one applies?”
251
+ *These places are exactly why showing reveals more than describing.*
252
+ 3. **Screenshots only one by one and only after asking.** If the text isn't enough at a point (a
253
+ click without a name, a sentence pointing at something visible), ask first: “At 0:26 the text
254
+ only says ‘…’. May I look at the screen at that point?” Yes → `kimal3 show picture 0:26`; it
255
+ reports “Picture: <path>”, and you look at **only this one** picture. No → ask what was to be
256
+ seen there.
257
+ 4. **Then ask — one question at a time, always with a suggestion:** first the contradictions, one
258
+ after the other. After that, what a single demonstration can never show: **how often and when**
259
+ (“I'd guess every Monday morning — is that right?”) · **what happens in an exception** (a
260
+ public holiday, an empty list, the system can't be reached) · **who the result goes to**.
261
+
262
+ ## 12. Write it into the plan and suggest a way to build it
263
+
264
+ Write `plan/task-shown.md` with these parts:
265
+
266
+ - **Goal** in one sentence
267
+ - **The steps**, numbered
268
+ - **Rules and exceptions** from the answers
269
+ - **What is still open**
270
+ - **Where it comes from:** demonstration of <date> (or: description of <date>, if nothing was
271
+ read)
272
+
273
+ The plan carries the content **itself** and doesn't point to files in the recording — that is
274
+ about to go. Typed text that looks like access data doesn't go into it.
275
+
276
+ Then say how you **would build it**: through the programs' interfaces (fetching the data
277
+ directly, sending the message directly), **not** by re-clicking the clicks — that way it doesn't
278
+ break when a screen changes. The building doesn't happen here; it is the next step in the
279
+ project, right away or in the next session with `/k3start`.
280
+
281
+ ## 13. Deleting the recording — only after a yes
282
+
283
+ Measure the size of the recording folder and suggest, with the real values. If the task is in
284
+ the plan (step 12):
285
+
286
+ > “The recording is 12.9 MB and lies in `material/aufnahmen/2026-10-19-0912`. I'd delete it —
287
+ > the description is in the plan, and the recording contains everything you typed. What I read
288
+ > from it stays in the conversation history on this computer. Is that alright?”
289
+
290
+ If the suggestion comes without a plan (no in step 10, or “once more with test customers”), leave
291
+ out the part about the plan:
292
+
293
+ > “The recording is 12.9 MB and lies in `material/aufnahmen/2026-10-19-0912`. I'd delete it —
294
+ > the recording contains everything you typed. What I read from it stays in the conversation
295
+ > history on this computer. Is that alright?”
296
+
297
+ - **Yes:** delete **exactly this one** folder — not `material/aufnahmen/` as a whole, not the
298
+ `.gitignore` and `.ignore` next to it — and check that it is gone.
299
+ - **No:** it stays. Say where, and that it doesn't end up in Git.
300
+
301
+ Finally, in two sentences without jargon, what is now in the project, and **one** concrete next
302
+ step.
@@ -24,7 +24,7 @@ say so briefly and skip it — no filler.
24
24
  - Is there a status file (`NEXT-STEPS.md` or `ROADMAP.md`) — a small file in which the
25
25
  latest status and the next step are written down? Then read it.
26
26
  - **Where the status file lives and what it is called.** The project rule applies **only inside a workplace**: if a folder `projects/` or `projekte/` sits directly in a folder `workbench-…` or `werkbank-…` (both spellings occur, also mixed) and the session is in or under a folder in it, that **project folder** is the place. Name there: under `projects/` it is `NEXT-STEPS.md`, under `projekte/` `WEITER-HIER.md`. **Everywhere else** (starter package, your own repos, even a folder `~/projects/my-repo`) it stays as before: the file lives in the folder you started in; if one exists, use it; if one has to be created, it is called `NEXT-STEPS.md` — or `WEITER-HIER.md` if the folder was created in German. If both exist, ask which one applies and write to neither until you have the answer. **Which one applies (in a workplace):** if the project has one, the project's applies. Otherwise an existing workbench note (`workbench-…/NEXT-STEPS.md` or `werkbank-…/WEITER-HIER.md` from the old template) applies — you keep reading and maintaining it. If neither exists, it is created **in the project**. **Never create a new status file in the workbench.**
27
- - **Are you in the workbench itself (`workbench-…` or `werkbank-…`), not inside a project?** If its `CLAUDE.md` has the section “First session” (German: “Erste Sitzung”), follow **it** — it takes priority over everything below. If it isn't there (any more), show the projects in `projects/` or `projekte/` (without `example-capture-invoices` and `beispiel-rechnungen-erfassen`) with the **first status line** of each one's status file, or “no status file yet” if there is none; an existing workbench note (`NEXT-STEPS.md` or `WEITER-HIER.md`) goes above them. Recommend **one** of them and ask **one** question about it.
27
+ - **Are you in the workbench itself (`workbench-…` or `werkbank-…`), not inside a project?** If its `CLAUDE.md` has the section “First session” (German: “Erste Sitzung”), follow **it** — it takes priority over everything below. If it isn't there (any more), show the projects in `projects/` or `projekte/` (without `example-capture-invoices` and `beispiel-rechnungen-erfassen`) with the **first status line** of each one's status file, or “no status file yet” if there is none; an existing workbench note (`NEXT-STEPS.md` or `WEITER-HIER.md`) goes above them. Recommend **one** of them and ask **one** question about it. If the person picks a project from the list or names a new task (then you first create the folder and status file in the project, never in the workbench), bring the session into the project folder: with `change_directory` (if it only appears as a name in your tool list, load it via the tool search) — and say first: “The Claude app will ask whether I may change the folder, and then whether you trust it — both are fine, click Allow each time. If you land in an empty conversation, type `/k3start` there; you will find this conversation again in the list on the left.” Without the tool, or if the person declines: give the exact project folder (not the workbench above it) — in the app “Choose folder” and pick that folder, in the terminal `/exit`, `cd '‹path›'`, `claude` — and then `/k3start`. For a new project, read `projects/CLAUDE.md` (German: `projekte/CLAUDE.md`) first: from now on it applies.
28
28
  - No status file? **First check whether there is a `.git` folder where the status file would live** (in a workplace the project folder, otherwise the folder you started in). If there is, this
29
29
  project keeps a version history, and `git log --oneline -10` plus `git status` show you the
30
30
  latest state.
@@ -0,0 +1,82 @@
1
+ # Gemeinsame Teile von werkzeug.py und starter.py: Dateinamen, Umgebung des Aufnehmers, Musterliste der Fehlergruende,
2
+ # JSON lesen/schreiben, Stopp einer Sitzung. Kein eigener Einstieg; laeuft mit dem Python des Aufnehmers.
3
+ import json
4
+ import os
5
+ import re
6
+ import time
7
+
8
+ # Der Aufnehmer bekommt einen eigenen Unterordner, in den sonst niemand schreibt: Beim Abschliessen versiegelt er JEDE
9
+ # Datei seines Ordners und prueft das Siegel danach nach. Lag unser Herzschlag (starter.json) mit darin, brach es
10
+ # (`capture_seal_failed`, Lauf 1 am 21.10.); dasselbe droht jeder Datei, die wir dort schreiben. Bericht, Protokoll,
11
+ # Abbruch-Datei, Zeitleiste und Bilder liegen deshalb eine Ebene hoeher.
12
+ ROH = "roh"
13
+ STARTER = "starter.json"
14
+ PROTOKOLL = "aufnahme.log"
15
+ ABBRECHEN = "abbrechen"
16
+ ZUSTAND_AUFNEHMER = "capture-state.json"
17
+ UMGEBUNG = {
18
+ "STOP_SEQUENCES": '[["o","a",".","s","t","o","p"]]', # die Strg-Folge des Aufnehmers beendet sonst bei Strg+C (B1)
19
+ "RECORD_AUDIO_RETAIN_WAVEFORM": "true",
20
+ "PYTHONUNBUFFERED": "1",
21
+ "PYTHONUTF8": "1",
22
+ }
23
+
24
+ # Zwilling von GRUENDE in cli/src/zeigen-rechnung.ts -- gleiche Reihenfolge, gleiche Schalter (der Test vergleicht beide).
25
+ GRUENDE = [
26
+ (re.compile(r"FFmpegUnavailableError|ffmpeg.*not found", re.IGNORECASE), "ffmpeg-fehlt"),
27
+ (re.compile(r"returned success without a non-empty output"), "pfad-zu-lang"),
28
+ (re.compile(r"PortAudio|No Default Input Device|sounddevice"), "kein-mikrofon"),
29
+ (re.compile(r"DesktopCaptureError|screen layout"), "bildschirm-gesperrt"),
30
+ (re.compile(r"did not become ready|did not start"), "startet-nicht"),
31
+ ]
32
+
33
+
34
+ def fehler_grund(protokoll: str) -> str:
35
+ return next((code for muster, code in GRUENDE if muster.search(protokoll)), "unbekannt")
36
+
37
+
38
+ def schreibe_json_atomar(pfad: str, daten: dict) -> None:
39
+ tmp = pfad + ".tmp"
40
+ with open(tmp, "w", encoding="utf-8") as f:
41
+ json.dump(daten, f)
42
+ for versuch in range(20):
43
+ try:
44
+ os.replace(tmp, pfad)
45
+ return
46
+ except PermissionError: # Windows: ein Leser haelt die Datei gerade offen
47
+ if versuch == 19:
48
+ raise
49
+ time.sleep(0.05)
50
+
51
+
52
+ def lies_json(pfad: str) -> dict | None:
53
+ try:
54
+ with open(pfad, encoding="utf-8") as f:
55
+ daten = json.load(f)
56
+ except (OSError, ValueError):
57
+ return None
58
+ return daten if isinstance(daten, dict) else None
59
+
60
+
61
+ def lies_protokoll(ordner: str) -> str:
62
+ try:
63
+ with open(os.path.join(ordner, PROTOKOLL), encoding="utf-8", errors="replace") as f:
64
+ return f.read()
65
+ except OSError:
66
+ return ""
67
+
68
+
69
+ def sauber(ordner: str) -> bool:
70
+ zustand = lies_json(os.path.join(ordner, ROH, ZUSTAND_AUFNEHMER)) or {}
71
+ return zustand.get("integrity_verified") is True and zustand.get("phase") == "complete"
72
+
73
+
74
+ def _stoppe_sitzung(sitzung: str, timeout: float) -> None:
75
+ """Stoppt genau diese Sitzung und kehrt erst nach dem geprueften Abschluss zurueck; sonst eine Ausnahme. Laut
76
+ Aufnehmer (control.stop_recording) ist ein Stopp idempotent: ein zweiter, gleichzeitiger wartet auf denselben
77
+ Abschluss. Nach Ablauf von `timeout` schliesst der Aufnehmer trotzdem weiter ab -- nur die Antwort fehlt."""
78
+ if not sitzung: # ohne Sitzung stoppt stop_recording die EINE laufende Aufnahme -- womoeglich eine fremde
79
+ raise ValueError("keine Sitzung")
80
+ from openadapt_capture.control import stop_recording # erst nach chdir: der Aufnehmer liest beim Import eine .env
81
+
82
+ stop_recording(sitzung, timeout=timeout)
@@ -0,0 +1,163 @@
1
+ # Starter hinter `werkzeug.py starten <konfig.json>`: haelt den Aufnehmer als eigenes Kind (versteckte Konsole), zieht die
2
+ # Bremse nach der Frist, beendet ihn bei `abbrechen` und schreibt laufend starter.json mit Herzschlag `zuletzt`.
3
+ # Von Node mit pythonw abgehaengt gestartet: keine Ausgabe. Einstieg bleibt werkzeug.py (Ruling R-12: eigene Datei).
4
+ import os
5
+ import re
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ import time
10
+
11
+ from gemeinsam import (ABBRECHEN, PROTOKOLL, ROH, STARTER, UMGEBUNG, _stoppe_sitzung, fehler_grund, lies_protokoll,
12
+ sauber, schreibe_json_atomar)
13
+
14
+ TAKT_SEK = 2.0 # so oft schaut der Starter nach Sitzung, Abbruch und Frist (Wanduhr, nicht ein Zeitgeber ueber 15 Minuten)
15
+ NACHTOETEN_SEK = 0.5 # Pause zwischen den ersten Versuchen, einen Prozessbaum zu beenden
16
+ NACHTOETEN_VERSUCHE = 3 # danach meldet der Starter "nicht-beendet" und versucht es jeden Takt weiter
17
+ # Video-Abschluss und eingebaute Spracherkennung nach 15 Minuten brauchen mehr als die Vorgabe 60 s. Der Starter
18
+ # laeuft ohne Claudes 10-Minuten-Grenze; `kimal3 zeigen stopp` wartet kuerzer (werkzeug.STOPP_SEK) und meldet dann
19
+ # "wird noch abgeschlossen".
20
+ BREMSE_STOPP_SEK = 600
21
+ AUFNEHMEN = "import sys; from openadapt_capture.cli import record; record(sys.argv[1], audio=True)"
22
+
23
+
24
+ def _starte_kind(befehl: list, ordner: str, protokoll) -> subprocess.Popen:
25
+ # Eigene, VERSTECKTE Konsole: Sonst oeffnen python.exe, sein multiprocessing-Kind und FFmpeg je ein Fenster
26
+ # (gemessen 18.10.: detached + windowsHide allein reicht nicht; starter-probe.py).
27
+ si = subprocess.STARTUPINFO()
28
+ si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
29
+ si.wShowWindow = 0 # SW_HIDE
30
+ return subprocess.Popen(befehl, cwd=ordner, env=dict(os.environ, **UMGEBUNG), stdout=protokoll,
31
+ stderr=subprocess.STDOUT, startupinfo=si, creationflags=subprocess.CREATE_NEW_CONSOLE)
32
+
33
+
34
+ def _beende_baum(kind: subprocess.Popen) -> bool:
35
+ """Beendet Kind samt Enkeln (multiprocessing, FFmpeg) und prueft, dass keiner mehr lebt."""
36
+ import psutil
37
+
38
+ try:
39
+ baum = [psutil.Process(kind.pid)]
40
+ baum += baum[0].children(recursive=True)
41
+ except psutil.NoSuchProcess:
42
+ baum = []
43
+ subprocess.run(["taskkill", "/PID", str(kind.pid), "/T", "/F"], capture_output=True,
44
+ creationflags=subprocess.CREATE_NO_WINDOW)
45
+ _, lebend = psutil.wait_procs(baum, timeout=10)
46
+ for p in lebend:
47
+ try:
48
+ p.kill()
49
+ except psutil.NoSuchProcess:
50
+ pass
51
+ _, lebend = psutil.wait_procs(lebend, timeout=5)
52
+ try:
53
+ kind.wait(timeout=5)
54
+ except subprocess.TimeoutExpired:
55
+ return False
56
+ return not lebend
57
+
58
+
59
+ def _mit_herzschlag(arbeit, melde):
60
+ """Fuehrt eine blockierende Arbeit (Stopp, Prozessbaum beenden) in einem Faden aus und schreibt waehrenddessen jeden
61
+ Takt `zuletzt` -- sonst haelt Node einen gesunden Starter, der gerade abschliesst, nach 10 s fuer tot."""
62
+ ergebnis = {}
63
+
64
+ def lauf():
65
+ try:
66
+ ergebnis["wert"] = arbeit()
67
+ except BaseException as fehler:
68
+ ergebnis["fehler"] = fehler
69
+
70
+ faden = threading.Thread(target=lauf, daemon=True)
71
+ faden.start()
72
+ while True:
73
+ faden.join(TAKT_SEK)
74
+ if not faden.is_alive():
75
+ break
76
+ melde()
77
+ if "fehler" in ergebnis:
78
+ raise ergebnis["fehler"]
79
+ return ergebnis.get("wert")
80
+
81
+
82
+ def _warte_auf_kind(kind: subprocess.Popen, sekunden: float, melde) -> bool:
83
+ """Wartet in Takt-Scheiben mit Herzschlag; True, wenn das Kind in der Zeit geendet hat."""
84
+ ende = time.time() + sekunden
85
+ while kind.poll() is None and time.time() < ende:
86
+ try:
87
+ kind.wait(timeout=TAKT_SEK)
88
+ except subprocess.TimeoutExpired:
89
+ melde()
90
+ return kind.poll() is not None
91
+
92
+
93
+ def _raeume_weg(kind: subprocess.Popen, melde) -> None:
94
+ """Kehrt erst zurueck, wenn der Baum nachweislich weg ist. Solange er lebt, bleibt der Starter da (Bremse bleibt
95
+ gezogen) und meldet `grund: nicht-beendet` -- nie einen Endzustand, waehrend noch etwas aufnimmt."""
96
+ versuch = 0
97
+ while not _mit_herzschlag(lambda: _beende_baum(kind), melde):
98
+ versuch += 1
99
+ if versuch < NACHTOETEN_VERSUCHE:
100
+ time.sleep(NACHTOETEN_SEK)
101
+ continue
102
+ melde(grund="nicht-beendet")
103
+ time.sleep(TAKT_SEK)
104
+
105
+
106
+ def starte(konfig: dict, befehl: list | None = None) -> None:
107
+ ordner = konfig["ordner"]
108
+ gestartet = time.time()
109
+ bericht = {"zustand": "startet", "sitzung": None, "gestartet": gestartet,
110
+ "frist": gestartet + float(konfig["hoechstens_sek"]), "grund": None, "zuletzt": gestartet}
111
+ pfad = os.path.join(ordner, STARTER)
112
+
113
+ def melde(**felder):
114
+ # `zuletzt` ist der Herzschlag: Node haelt einen Starter, der vor <= 10 s schrieb, fuer lebendig (Ruling R-3).
115
+ bericht.update(felder, zuletzt=time.time())
116
+ schreibe_json_atomar(pfad, bericht)
117
+
118
+ melde()
119
+ kind = None
120
+ try:
121
+ python = os.path.join(os.path.dirname(sys.executable), "python.exe")
122
+ roh = os.path.join(ordner, ROH) # nur der Aufnehmer schreibt dort (gemeinsam.ROH)
123
+ os.makedirs(roh, exist_ok=True)
124
+ with open(os.path.join(ordner, PROTOKOLL), "w", encoding="utf-8") as protokoll:
125
+ kind = _starte_kind(befehl or [python, "-c", AUFNEHMEN, roh], roh, protokoll)
126
+ while True:
127
+ try:
128
+ kind.wait(timeout=TAKT_SEK)
129
+ except subprocess.TimeoutExpired:
130
+ pass
131
+ treffer = None if bericht["sitzung"] else re.search(r"Capture session: ([0-9a-f-]{36})", lies_protokoll(ordner))
132
+ melde(**({"zustand": "laeuft", "sitzung": treffer.group(1)} if treffer else {}))
133
+ if kind.poll() is not None:
134
+ return melde(**_ende(ordner, bericht["sitzung"], "gestoppt"))
135
+ if os.path.exists(os.path.join(ordner, ABBRECHEN)):
136
+ _raeume_weg(kind, melde)
137
+ return melde(zustand="abgebrochen", grund=None)
138
+ if time.time() >= bericht["frist"]:
139
+ if bericht["sitzung"] is None:
140
+ _raeume_weg(kind, melde)
141
+ return melde(zustand="fehlgeschlagen", grund=fehler_grund(lies_protokoll(ordner)))
142
+ try:
143
+ _mit_herzschlag(lambda: _stoppe_sitzung(bericht.get("sitzung"), BREMSE_STOPP_SEK), melde)
144
+ except Exception:
145
+ pass # ob die Aufnahme sauber zu ist, sagt danach capture-state.json (_ende)
146
+ if not _warte_auf_kind(kind, 120, melde):
147
+ _raeume_weg(kind, melde)
148
+ return melde(**_ende(ordner, bericht["sitzung"], "selbst-gestoppt"))
149
+ except Exception:
150
+ grund = "unbekannt"
151
+ if kind is not None and kind.poll() is None:
152
+ try:
153
+ _raeume_weg(kind, melde)
154
+ except Exception:
155
+ grund = "nicht-beendet" # nicht nachgewiesen weg: Node sagt "kann ich nicht feststellen", nie "beendet"
156
+ melde(zustand="fehlgeschlagen", grund=grund)
157
+
158
+
159
+ def _ende(ordner: str, sitzung: str | None, art: str) -> dict:
160
+ """Das Kind ist weg: sauber abgeschlossen -> `art`; mit Sitzung, aber nicht sauber -> abgebrochen; ohne -> fehlgeschlagen."""
161
+ if sitzung is not None and sauber(ordner):
162
+ return {"zustand": art, "grund": None}
163
+ return {"zustand": "abgebrochen" if sitzung else "fehlgeschlagen", "grund": fehler_grund(lies_protokoll(ordner))}