chore: prepare open-source release with docs, metadata, and tests

This commit is contained in:
yangzhuangqiu
2026-03-28 16:40:26 +08:00
parent 9b081f84a3
commit 50083f1e7f
9 changed files with 404 additions and 66 deletions
+4
View File
@@ -206,9 +206,13 @@ marimo/_static/
marimo/_lsp/
__marimo__/
*/.*
.*
!.gitignore
# ComfyUI-Simple-Iterator runtime state
.iterator_state.json
# Test temp state
tests/.tmp/
tests/.tmp_manual/
+20
View File
@@ -0,0 +1,20 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.0] - 2026-03-28
### Added
- Added persistent iterator state with scope isolation by node id and source key.
- Added `Iterator Load Image` with one-sample-per-run behavior and file name output.
- Added `Iterator Load Video Path` with one-sample-per-run behavior and file name output.
- Added `Iterator Load Text From Dir` and `Iterator Load Text From File`.
- Added loop modes: `loop`, `stop`, and `hold_last`.
- Added edge-triggered reset behavior (`False -> True` only).
- Added runtime state ignore rule for `.iterator_state.json`.
- Added minimal tests for sequence progression, stop mode, edge reset, and filename output behavior.
### Changed
- Split text loading into two dedicated nodes and removed the combined text node.
- Updated defaults for image/video/text patterns and loop behavior to safer batch-processing defaults.
- Expanded inline node documentation and README usage details.
+104 -63
View File
@@ -1,20 +1,29 @@
# ComfyUI-Simple-Iterator
ComfyUI custom nodes for one-sample-per-run batch iteration.
ComfyUI custom nodes for one-sample-per-run iteration across image, video, and text sources.
Language: **English** | [简体中文](./README.zh-CN.md)
## Features
- Persistent iterator cursor state (`.iterator_state.json`)
- Scope isolation by node id + source settings
- Edge-triggered reset (`False -> True`)
- Safe stop mode to prevent silent duplicate processing
- File name output with or without extension
## Common Semantics
- `loop_mode`
- `loop`: 到末尾后回到第一个继续。
- `stop`: 到末尾后下一次执行报错停止,避免重复处理。
- `hold_last`: 到末尾后持续返回最后一个样本。
- `loop`: wrap to the first item after the last item
- `stop`: raise an error when exhausted (recommended for batch jobs)
- `hold_last`: keep returning the last item after exhaustion
- `reset`
- 沿触发复位(edge trigger),仅在 `False -> True` 时复位一次。
- 例如 `Batch Count = 10` 时,如果 `reset` 全程保持 `True`,只会在第一轮复位。
- `load_always`(仅图片/视频节点)
- `False`: 允许缓存判定,未变化时可复用结果。
- `True`: 强制每次都执行节点,绕过缓存判定。
- “每次”按节点执行次数计算;通常 `Batch Count = 10` 会触发 10 次执行。
- Edge-triggered reset only
- Reset happens only when `reset` changes from `False` to `True`
- `load_always` (image/video nodes)
- `False`: allow normal cache checks
- `True`: force node execution each run
## Nodes
@@ -26,25 +35,25 @@ Supports: `.jpg/.jpeg/.png/.webp/.bmp/.tif/.tiff`
| Name | Type | Default | Meaning |
|---|---|---|---|
| `directory` | `STRING` | `""` | 图片目录路径。 |
| `pattern` | `STRING` | `"*.png,*.jpg,*.jpeg,*.webp"` | 通配符过滤,支持 `,`/`;` 分隔多个模式。 |
| `recursive` | `BOOLEAN` | `False` | 是否递归扫描子目录。 |
| `order` | `ENUM` | `name_asc` | 排序方式:`name_asc/name_desc/mtime_asc/mtime_desc`。 |
| `loop_mode` | `ENUM` | `stop` | 耗尽策略:`loop/stop/hold_last`。 |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号,仅 `False->True` 生效一次。 |
| `load_always` | `BOOLEAN` | `False` | 是否每次都强制执行节点。 |
| `filename_with_ext` | `BOOLEAN` | `True` | `FILE_NAME` 是否带后缀。 |
| `directory` | `STRING` | `""` | Image directory path. |
| `pattern` | `STRING` | `"*.png,*.jpg,*.jpeg,*.webp"` | Glob patterns, supports comma/semicolon separated values. |
| `recursive` | `BOOLEAN` | `False` | Recursively scan subdirectories. |
| `order` | `ENUM` | `name_asc` | `name_asc/name_desc/mtime_asc/mtime_desc`. |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last`. |
| `reset` | `BOOLEAN` | `False` | Edge-triggered reset signal. |
| `load_always` | `BOOLEAN` | `False` | Force execution every run. |
| `filename_with_ext` | `BOOLEAN` | `True` | Whether `FILE_NAME` includes extension. |
#### Outputs
| Name | Type | Meaning |
|---|---|---|
| `IMAGE` | `IMAGE` | 当前图片张量(RGB)。 |
| `MASK` | `MASK` | 当前图片掩码(无 alpha 时全 0)。 |
| `FILE_PATH` | `STRING` | 当前图片完整路径。 |
| `FILE_NAME` | `STRING` | 当前图片文件名(受 `filename_with_ext` 控制)。 |
| `INDEX` | `INT` | 当前索引(从 0 开始)。 |
| `TOTAL` | `INT` | 样本总数。 |
| `IMAGE` | `IMAGE` | Current image tensor (RGB). |
| `MASK` | `MASK` | Current mask tensor (all zeros if no alpha channel). |
| `FILE_PATH` | `STRING` | Full path of current image. |
| `FILE_NAME` | `STRING` | File name with or without extension. |
| `INDEX` | `INT` | Current index (0-based). |
| `TOTAL` | `INT` | Total available items. |
### Iterator Load Video Path
@@ -54,77 +63,109 @@ Supports: `.mp4/.mov/.mkv/.avi/.webm/.m4v/.mpg/.mpeg/.wmv/.flv`
| Name | Type | Default | Meaning |
|---|---|---|---|
| `directory` | `STRING` | `""` | 视频目录路径。 |
| `pattern` | `STRING` | `"*.mp4"` | 通配符过滤,支持 `,`/`;` 分隔多个模式。 |
| `recursive` | `BOOLEAN` | `False` | 是否递归扫描子目录。 |
| `order` | `ENUM` | `name_asc` | 排序方式:`name_asc/name_desc/mtime_asc/mtime_desc`。 |
| `loop_mode` | `ENUM` | `stop` | 耗尽策略:`loop/stop/hold_last`。 |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号,仅 `False->True` 生效一次。 |
| `load_always` | `BOOLEAN` | `False` | 是否每次都强制执行节点。 |
| `filename_with_ext` | `BOOLEAN` | `True` | `FILE_NAME` 是否带后缀。 |
| `directory` | `STRING` | `""` | Video directory path. |
| `pattern` | `STRING` | `"*.mp4"` | Glob patterns, supports comma/semicolon separated values. |
| `recursive` | `BOOLEAN` | `False` | Recursively scan subdirectories. |
| `order` | `ENUM` | `name_asc` | `name_asc/name_desc/mtime_asc/mtime_desc`. |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last`. |
| `reset` | `BOOLEAN` | `False` | Edge-triggered reset signal. |
| `load_always` | `BOOLEAN` | `False` | Force execution every run. |
| `filename_with_ext` | `BOOLEAN` | `True` | Whether `FILE_NAME` includes extension. |
#### Outputs
| Name | Type | Meaning |
|---|---|---|
| `VIDEO_PATH` | `STRING` | 当前视频完整路径。 |
| `FILE_NAME` | `STRING` | 当前视频文件名(受 `filename_with_ext` 控制)。 |
| `INDEX` | `INT` | 当前索引(从 0 开始)。 |
| `TOTAL` | `INT` | 样本总数。 |
| `VIDEO_PATH` | `STRING` | Full path of current video. |
| `FILE_NAME` | `STRING` | File name with or without extension. |
| `INDEX` | `INT` | Current index (0-based). |
| `TOTAL` | `INT` | Total available items. |
### Iterator Load Text From Dir
Directory mode: 每个文本文件整体作为一个输出项。
Each text file is emitted as one output item.
Supports: `.txt/.md/.prompt/.json/.jsonl`
#### Inputs
| Name | Type | Default | Meaning |
|---|---|---|---|
| `directory` | `STRING` | `""` | 文本目录路径。 |
| `pattern` | `STRING` | `"*.txt,*.md,*.prompt,*.json,*.jsonl"` | 通配符过滤,支持 `,`/`;` 分隔多个模式。 |
| `recursive` | `BOOLEAN` | `False` | 是否递归扫描子目录。 |
| `order` | `ENUM` | `name_asc` | 排序方式:`name_asc/name_desc/mtime_asc/mtime_desc`。 |
| `encoding` | `STRING` | `"utf-8"` | 文本读取编码。 |
| `loop_mode` | `ENUM` | `stop` | 耗尽策略:`loop/stop/hold_last`。 |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号,仅 `False->True` 生效一次。 |
| `directory` | `STRING` | `""` | Text directory path. |
| `pattern` | `STRING` | `"*.txt,*.md,*.prompt,*.json,*.jsonl"` | Glob patterns, supports comma/semicolon separated values. |
| `recursive` | `BOOLEAN` | `False` | Recursively scan subdirectories. |
| `order` | `ENUM` | `name_asc` | `name_asc/name_desc/mtime_asc/mtime_desc`. |
| `encoding` | `STRING` | `"utf-8"` | File encoding. |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last`. |
| `reset` | `BOOLEAN` | `False` | Edge-triggered reset signal. |
#### Outputs
| Name | Type | Meaning |
|---|---|---|
| `TEXT` | `STRING` | 当前文本文件内容。 |
| `SOURCE_PATH` | `STRING` | 当前文本文件完整路径。 |
| `INDEX` | `INT` | 当前索引(从 0 开始)。 |
| `TOTAL` | `INT` | 样本总数。 |
| `TEXT` | `STRING` | Full text content of current file. |
| `SOURCE_PATH` | `STRING` | Full path of current file. |
| `INDEX` | `INT` | Current index (0-based). |
| `TOTAL` | `INT` | Total available items. |
### Iterator Load Text From File
File mode: 从单文件切分或解析多条文本。
Loads multiple text items from a single file.
#### Inputs
| Name | Type | Default | Meaning |
|---|---|---|---|
| `file_path` | `STRING` | `""` | 文本文件路径。 |
| `parse_mode` | `ENUM` | `file_split` | 解析方式:`file_split/json_array/jsonl`。 |
| `delimiter` | `STRING` | `"\\n---\\n"` | `file_split` 分隔符,支持转义字符。 |
| `json_field` | `STRING` | `""` | `json_array/jsonl` 对象项提取字段,支持点路径。 |
| `encoding` | `STRING` | `"utf-8"` | 文本读取编码。 |
| `loop_mode` | `ENUM` | `stop` | 耗尽策略:`loop/stop/hold_last`。 |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号,仅 `False->True` 生效一次。 |
| `file_path` | `STRING` | `""` | Input file path. |
| `parse_mode` | `ENUM` | `file_split` | `file_split/json_array/jsonl`. |
| `delimiter` | `STRING` | `"\\n---\\n"` | Delimiter for `file_split` mode (supports escaped characters). |
| `json_field` | `STRING` | `""` | Field path for object entries (dot path supported). |
| `encoding` | `STRING` | `"utf-8"` | File encoding. |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last`. |
| `reset` | `BOOLEAN` | `False` | Edge-triggered reset signal. |
#### Outputs
| Name | Type | Meaning |
|---|---|---|
| `TEXT` | `STRING` | 当前文本片段或记录内容。 |
| `SOURCE_PATH` | `STRING` | 来源位置(`#分片索引` 或 `:行号`)。 |
| `INDEX` | `INT` | 当前索引(从 0 开始)。 |
| `TOTAL` | `INT` | 样本总数。 |
| `TEXT` | `STRING` | Current text segment or record. |
| `SOURCE_PATH` | `STRING` | Source marker (`#segment` or `:line`). |
| `INDEX` | `INT` | Current index (0-based). |
| `TOTAL` | `INT` | Total available items. |
## Cursor State
## Minimal Workflow Examples
Cursor state is persisted in `.iterator_state.json`.
1. Image batch processing
- `Iterator Load Image` -> existing image workflow head node
2. Video batch processing
- `Iterator Load Video Path` -> your video path-based loader node
3. Prompt/text batch processing
- `Iterator Load Text From Dir` or `Iterator Load Text From File` -> prompt input branch
Scope is isolated by node unique id + source settings, so different nodes do not share the same cursor.
## Installation
1. Clone this repo into your ComfyUI `custom_nodes` directory.
2. Install runtime dependencies in the same Python environment used by ComfyUI:
```bash
python -m pip install -r requirements.txt
```
3. Restart ComfyUI.
Notes:
- `torch` is expected to be provided by your ComfyUI environment.
- Keep your `torch` version aligned with your ComfyUI setup.
## Testing
Run tests:
```bash
python -m pip install pytest
python -m pytest
```
## State File
- Cursor state is stored in `.iterator_state.json`
- The file is ignored by git
+169
View File
@@ -0,0 +1,169 @@
# ComfyUI-Simple-Iterator
用于图片、视频、文本“每次运行只输出一个样本”的 ComfyUI 自定义节点集。
语言: [English](./README.md) | **简体中文**
## 功能特性
- 持久化游标状态(`.iterator_state.json`)
- 按节点 id + 数据源配置隔离状态
- `reset` 沿触发复位(`False -> True`)
- `stop` 模式防止批处理中静默重复
- 支持输出带后缀或不带后缀的文件名
## 通用语义
- `loop_mode`
- `loop`:到末尾后回到第一项
- `stop`:到末尾后下一次执行报错停止(批处理推荐)
- `hold_last`:到末尾后持续返回最后一项
- `reset`
- 仅沿触发复位
- 只有从 `False` 变为 `True` 时才复位
- `load_always`(图片/视频节点)
- `False`:按正常缓存判定
- `True`:每次都强制执行
## 节点说明
### Iterator Load Image
支持后缀:`.jpg/.jpeg/.png/.webp/.bmp/.tif/.tiff`
#### 输入参数
| 名称 | 类型 | 默认值 | 含义 |
|---|---|---|---|
| `directory` | `STRING` | `""` | 图片目录路径 |
| `pattern` | `STRING` | `"*.png,*.jpg,*.jpeg,*.webp"` | 文件匹配模式,支持逗号/分号分隔 |
| `recursive` | `BOOLEAN` | `False` | 是否递归扫描子目录 |
| `order` | `ENUM` | `name_asc` | `name_asc/name_desc/mtime_asc/mtime_desc` |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last` |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号 |
| `load_always` | `BOOLEAN` | `False` | 是否每次强制执行 |
| `filename_with_ext` | `BOOLEAN` | `True` | `FILE_NAME` 是否带后缀 |
#### 输出参数
| 名称 | 类型 | 含义 |
|---|---|---|
| `IMAGE` | `IMAGE` | 当前图片张量(RGB) |
| `MASK` | `MASK` | 当前掩码(无 alpha 时全 0) |
| `FILE_PATH` | `STRING` | 当前图片完整路径 |
| `FILE_NAME` | `STRING` | 当前文件名(可带后缀) |
| `INDEX` | `INT` | 当前索引(0 开始) |
| `TOTAL` | `INT` | 样本总数 |
### Iterator Load Video Path
支持后缀:`.mp4/.mov/.mkv/.avi/.webm/.m4v/.mpg/.mpeg/.wmv/.flv`
#### 输入参数
| 名称 | 类型 | 默认值 | 含义 |
|---|---|---|---|
| `directory` | `STRING` | `""` | 视频目录路径 |
| `pattern` | `STRING` | `"*.mp4"` | 文件匹配模式,支持逗号/分号分隔 |
| `recursive` | `BOOLEAN` | `False` | 是否递归扫描子目录 |
| `order` | `ENUM` | `name_asc` | `name_asc/name_desc/mtime_asc/mtime_desc` |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last` |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号 |
| `load_always` | `BOOLEAN` | `False` | 是否每次强制执行 |
| `filename_with_ext` | `BOOLEAN` | `True` | `FILE_NAME` 是否带后缀 |
#### 输出参数
| 名称 | 类型 | 含义 |
|---|---|---|
| `VIDEO_PATH` | `STRING` | 当前视频完整路径 |
| `FILE_NAME` | `STRING` | 当前文件名(可带后缀) |
| `INDEX` | `INT` | 当前索引(0 开始) |
| `TOTAL` | `INT` | 样本总数 |
### Iterator Load Text From Dir
目录模式:每个文本文件整体作为一条输出。
支持后缀:`.txt/.md/.prompt/.json/.jsonl`
#### 输入参数
| 名称 | 类型 | 默认值 | 含义 |
|---|---|---|---|
| `directory` | `STRING` | `""` | 文本目录路径 |
| `pattern` | `STRING` | `"*.txt,*.md,*.prompt,*.json,*.jsonl"` | 文件匹配模式,支持逗号/分号分隔 |
| `recursive` | `BOOLEAN` | `False` | 是否递归扫描子目录 |
| `order` | `ENUM` | `name_asc` | `name_asc/name_desc/mtime_asc/mtime_desc` |
| `encoding` | `STRING` | `"utf-8"` | 文件编码 |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last` |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号 |
#### 输出参数
| 名称 | 类型 | 含义 |
|---|---|---|
| `TEXT` | `STRING` | 当前文本内容(整文件) |
| `SOURCE_PATH` | `STRING` | 当前文件完整路径 |
| `INDEX` | `INT` | 当前索引(0 开始) |
| `TOTAL` | `INT` | 样本总数 |
### Iterator Load Text From File
单文件模式:从一个文件中拆分/解析出多条文本。
#### 输入参数
| 名称 | 类型 | 默认值 | 含义 |
|---|---|---|---|
| `file_path` | `STRING` | `""` | 输入文件路径 |
| `parse_mode` | `ENUM` | `file_split` | `file_split/json_array/jsonl` |
| `delimiter` | `STRING` | `"\\n---\\n"` | `file_split` 分隔符(支持转义) |
| `json_field` | `STRING` | `""` | 对象记录取值字段(支持点路径) |
| `encoding` | `STRING` | `"utf-8"` | 文件编码 |
| `loop_mode` | `ENUM` | `stop` | `loop/stop/hold_last` |
| `reset` | `BOOLEAN` | `False` | 沿触发复位信号 |
#### 输出参数
| 名称 | 类型 | 含义 |
|---|---|---|
| `TEXT` | `STRING` | 当前文本片段/记录 |
| `SOURCE_PATH` | `STRING` | 来源标记(`#分片` 或 `:行号`) |
| `INDEX` | `INT` | 当前索引(0 开始) |
| `TOTAL` | `INT` | 样本总数 |
## 最小工作流示例
1. 图片批处理
`Iterator Load Image` -> 现有图片工作流入口
2. 视频批处理
`Iterator Load Video Path` -> 你的视频路径加载节点
3. 文本批处理
`Iterator Load Text From Dir` 或 `Iterator Load Text From File` -> prompt 分支
## 安装
1. 将本仓库克隆到 ComfyUI 的 `custom_nodes` 目录。
2. 在与 ComfyUI 相同的 Python 环境中安装运行依赖:
```bash
python -m pip install -r requirements.txt
```
3. 重启 ComfyUI。
说明:
- `torch` 默认由 ComfyUI 运行环境提供。
- 请保持 `torch` 与你的 ComfyUI 环境版本一致。
## 测试
```bash
python -m pip install pytest
python -m pytest
```
## 状态文件
- 游标状态保存在 `.iterator_state.json`
- 该文件已加入 git ignore
+5
View File
@@ -20,6 +20,11 @@ def stable_scope(kind: str, unique_id: str, source_key: str) -> str:
return _sha1((kind, unique_id or "default", source_key))
def format_output_filename(path: Path, with_ext: bool) -> str:
"""根据开关返回文件名(带后缀或不带后缀)。"""
return path.name if with_ext else path.stem
class IteratorStateStore:
"""定义迭代游标的持久化存取契约。
+3 -3
View File
@@ -8,7 +8,7 @@ import numpy as np
import torch
from PIL import Image, ImageOps
from .iterator_core import IteratorStateStore, stable_scope
from .iterator_core import IteratorStateStore, format_output_filename, stable_scope
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff", ".tif"}
@@ -428,7 +428,7 @@ class IteratorLoadImage:
)
selected = paths[index]
image, mask = _load_image_tensor(selected)
file_name = selected.name if filename_with_ext else selected.stem
file_name = format_output_filename(selected, filename_with_ext)
return image, mask, str(selected), file_name, index, len(paths)
@@ -534,7 +534,7 @@ class IteratorLoadVideoPath:
scope_key=scope_key, total=len(paths), reset=reset, loop_mode=loop_mode
)
selected = paths[index]
file_name = selected.name if filename_with_ext else selected.stem
file_name = format_output_filename(selected, filename_with_ext)
return str(selected), file_name, index, len(paths)
+21
View File
@@ -0,0 +1,21 @@
[project]
name = "ComfyUI-Simple-Iterator"
version = "0.1.0"
description = "ComfyUI custom nodes for one-sample-per-run iteration across image, video, and text sources."
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
dependencies = [
"numpy>=1.24",
"Pillow>=10.0",
]
[project.optional-dependencies]
test = [
"pytest>=8.0",
]
[tool.comfy]
PublisherId = "simple-iterator"
DisplayName = "ComfyUI-Simple-Iterator"
Icon = ""
+2
View File
@@ -0,0 +1,2 @@
numpy>=1.24
Pillow>=10.0
+76
View File
@@ -0,0 +1,76 @@
import shutil
import uuid
from pathlib import Path
import pytest
from iterator_core import IteratorStateStore, format_output_filename, stable_scope
def _reset_store(state_path: Path) -> None:
state_path.parent.mkdir(parents=True, exist_ok=True)
IteratorStateStore._state_file = state_path
IteratorStateStore._state = {}
IteratorStateStore._loaded = False
def _state_path() -> Path:
return Path(__file__).resolve().parent / ".tmp" / f"state-{uuid.uuid4().hex}.json"
def _cleanup(path: Path) -> None:
if path.parent.exists():
shutil.rmtree(path.parent, ignore_errors=True)
def test_sequence_progression():
state_path = _state_path()
_reset_store(state_path)
scope = stable_scope("video", "node-a", "source-a")
indexes = [
IteratorStateStore.claim_index(scope_key=scope, total=3, reset=False, loop_mode="loop"),
IteratorStateStore.claim_index(scope_key=scope, total=3, reset=False, loop_mode="loop"),
IteratorStateStore.claim_index(scope_key=scope, total=3, reset=False, loop_mode="loop"),
]
assert indexes == [0, 1, 2]
_cleanup(state_path)
def test_stop_mode_raises_when_exhausted():
state_path = _state_path()
_reset_store(state_path)
scope = stable_scope("video", "node-b", "source-b")
IteratorStateStore.claim_index(scope_key=scope, total=2, reset=False, loop_mode="stop")
IteratorStateStore.claim_index(scope_key=scope, total=2, reset=False, loop_mode="stop")
with pytest.raises(RuntimeError, match="Iterator exhausted"):
IteratorStateStore.claim_index(scope_key=scope, total=2, reset=False, loop_mode="stop")
_cleanup(state_path)
def test_reset_edge_trigger():
state_path = _state_path()
_reset_store(state_path)
scope = stable_scope("video", "node-c", "source-c")
# First run, no reset: output index 0.
i0 = IteratorStateStore.claim_index(scope_key=scope, total=5, reset=False, loop_mode="loop")
# Rising edge False -> True: reset once, output index 0.
i1 = IteratorStateStore.claim_index(scope_key=scope, total=5, reset=True, loop_mode="loop")
# Keep True: no second reset, should continue to index 1.
i2 = IteratorStateStore.claim_index(scope_key=scope, total=5, reset=True, loop_mode="loop")
# Arm the next reset by setting False, then rising edge again.
IteratorStateStore.claim_index(scope_key=scope, total=5, reset=False, loop_mode="loop")
i3 = IteratorStateStore.claim_index(scope_key=scope, total=5, reset=True, loop_mode="loop")
assert (i0, i1, i2, i3) == (0, 0, 1, 0)
_cleanup(state_path)
def test_filename_output_with_and_without_extension():
sample = Path("demo_video.mp4")
assert format_output_filename(sample, True) == "demo_video.mp4"
assert format_output_filename(sample, False) == "demo_video"