Publish current ComfyUI O1Key code baseline
Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
"""Offline tests for the MiniMax-H3 node and New API response parsing."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from inspect import signature
|
||||
from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
|
||||
PLUGIN_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CUSTOM_NODES_DIR = os.path.dirname(PLUGIN_DIR)
|
||||
COMFY_ROOT = os.environ.get(
|
||||
"COMFYUI_ROOT",
|
||||
r"F:\ComfyUI_windows_portable\ComfyUI",
|
||||
)
|
||||
sys.path.insert(0, COMFY_ROOT)
|
||||
sys.path.insert(0, CUSTOM_NODES_DIR)
|
||||
|
||||
from comfyui_o1key.clients.minimax_h3_client import ( # noqa: E402
|
||||
FAILURE_STATUSES,
|
||||
MiniMaxH3Client,
|
||||
PENDING_STATUSES,
|
||||
SUCCESS_STATUSES,
|
||||
extract_public_task_id,
|
||||
parse_task_snapshot,
|
||||
)
|
||||
from comfyui_o1key.clients.newapi_veo_client import NewAPIVeoClient # noqa: E402
|
||||
from comfyui_o1key.nodes.minimax_h3_video import ( # noqa: E402
|
||||
MAX_SEED,
|
||||
MODEL_MAX_ID,
|
||||
MODE_FIRST,
|
||||
MODE_FIRST_LAST,
|
||||
MODE_LAST,
|
||||
MODE_REFERENCE,
|
||||
MODE_TEXT,
|
||||
MiniMaxH3Video,
|
||||
_make_progress_callbacks,
|
||||
build_request_body,
|
||||
)
|
||||
from comfyui_o1key.utils.minimax_h3_media import ( # noqa: E402
|
||||
inspect_audio,
|
||||
probe_video,
|
||||
validate_reference_audios,
|
||||
validate_reference_videos,
|
||||
validate_image,
|
||||
validate_video_info,
|
||||
)
|
||||
from comfyui_o1key.utils.video_task import POLL_DEADLINE_SECONDS # noqa: E402
|
||||
|
||||
|
||||
class MiniMaxH3PayloadTests(unittest.TestCase):
|
||||
def test_text_to_video_payload(self):
|
||||
body = build_request_body(
|
||||
prompt="月球上的宇航员",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_TEXT,
|
||||
ratio="16:9",
|
||||
)
|
||||
self.assertEqual(body["model"], "MiniMax-H3")
|
||||
self.assertEqual(body["seed"], 0)
|
||||
self.assertEqual(body["ratio"], "16:9")
|
||||
self.assertEqual(body["content"], [{"type": "text", "text": "月球上的宇航员"}])
|
||||
self.assertNotIn("callback_url", body)
|
||||
self.assertNotIn("aigc_watermark", body)
|
||||
|
||||
def test_native_seed_is_forwarded_and_validated(self):
|
||||
body = build_request_body(
|
||||
prompt="固定镜头",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_TEXT,
|
||||
ratio="16:9",
|
||||
seed=123456789,
|
||||
)
|
||||
self.assertEqual(body["seed"], 123456789)
|
||||
|
||||
for invalid_seed in (-1, MAX_SEED + 1, True, 1.5):
|
||||
with self.subTest(seed=invalid_seed), self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"seed 必须是",
|
||||
):
|
||||
build_request_body(
|
||||
prompt="x",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_TEXT,
|
||||
ratio="16:9",
|
||||
seed=invalid_seed,
|
||||
)
|
||||
|
||||
def test_first_frame_payload_uses_adaptive(self):
|
||||
body = build_request_body(
|
||||
prompt="镜头缓慢推进",
|
||||
resolution="768P",
|
||||
duration=4,
|
||||
mode=MODE_FIRST,
|
||||
first_url="https://cdn.example.com/first.png",
|
||||
)
|
||||
self.assertEqual(body["ratio"], "adaptive")
|
||||
self.assertEqual(body["content"][1]["role"], "first_frame")
|
||||
|
||||
def test_first_last_payload_roles(self):
|
||||
body = build_request_body(
|
||||
prompt="自然过渡",
|
||||
resolution="2K",
|
||||
duration=15,
|
||||
mode=MODE_FIRST_LAST,
|
||||
first_url="https://cdn.example.com/first.png",
|
||||
last_url="https://cdn.example.com/last.png",
|
||||
)
|
||||
self.assertEqual(
|
||||
[item.get("role") for item in body["content"][1:]],
|
||||
["first_frame", "last_frame"],
|
||||
)
|
||||
|
||||
def test_last_frame_only_payload(self):
|
||||
body = build_request_body(
|
||||
prompt="镜头最终停在城市夜景",
|
||||
resolution="2K",
|
||||
duration=6,
|
||||
mode=MODE_LAST,
|
||||
last_url="https://cdn.example.com/last.png",
|
||||
)
|
||||
self.assertEqual(body["ratio"], "adaptive")
|
||||
self.assertEqual(body["content"][1]["role"], "last_frame")
|
||||
|
||||
def test_reference_payload_roles(self):
|
||||
body = build_request_body(
|
||||
prompt="保持参考人物外观",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_REFERENCE,
|
||||
ratio="4:3",
|
||||
reference_image_urls=[
|
||||
"https://cdn.example.com/person.png",
|
||||
"https://cdn.example.com/style.png",
|
||||
],
|
||||
reference_video_urls=["https://cdn.example.com/motion.mp4"],
|
||||
reference_audio_urls=["https://cdn.example.com/voice.wav"],
|
||||
)
|
||||
self.assertEqual(body["ratio"], "4:3")
|
||||
self.assertEqual(
|
||||
[item.get("role") for item in body["content"][1:]],
|
||||
["reference_image", "reference_image", "reference_video", "reference_audio"],
|
||||
)
|
||||
|
||||
def test_reference_ratio_defaults_to_adaptive(self):
|
||||
body = build_request_body(
|
||||
prompt="保持参考风格",
|
||||
resolution="768P",
|
||||
duration=4,
|
||||
mode=MODE_REFERENCE,
|
||||
reference_image_urls=["https://cdn.example.com/person.png"],
|
||||
)
|
||||
self.assertEqual(body["ratio"], "adaptive")
|
||||
|
||||
def test_reference_max_counts(self):
|
||||
body = build_request_body(
|
||||
prompt="多素材参考",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_REFERENCE,
|
||||
reference_image_urls=[f"https://cdn.example.com/image-{i}.png" for i in range(6)],
|
||||
reference_video_urls=[f"https://cdn.example.com/video-{i}.mp4" for i in range(3)],
|
||||
reference_audio_urls=[f"https://cdn.example.com/audio-{i}.wav" for i in range(3)],
|
||||
)
|
||||
roles = [item.get("role") for item in body["content"][1:]]
|
||||
self.assertEqual(roles.count("reference_image"), 6)
|
||||
self.assertEqual(roles.count("reference_video"), 3)
|
||||
self.assertEqual(roles.count("reference_audio"), 3)
|
||||
|
||||
def test_reference_material_total_cannot_exceed_twelve(self):
|
||||
with self.assertRaisesRegex(ValueError, "合计最多 12 个"):
|
||||
build_request_body(
|
||||
prompt="多素材参考",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_REFERENCE,
|
||||
reference_image_urls=[
|
||||
f"https://cdn.example.com/image-{i}.png" for i in range(9)
|
||||
],
|
||||
reference_video_urls=[
|
||||
f"https://cdn.example.com/video-{i}.mp4" for i in range(3)
|
||||
],
|
||||
reference_audio_urls=["https://cdn.example.com/audio.wav"],
|
||||
)
|
||||
|
||||
def test_h3_max_payload_and_model_specific_limits(self):
|
||||
body = build_request_body(
|
||||
prompt="电影感城市延时",
|
||||
model=MODEL_MAX_ID,
|
||||
resolution="480P",
|
||||
duration=5,
|
||||
mode=MODE_TEXT,
|
||||
ratio="16:9",
|
||||
)
|
||||
self.assertEqual(body["model"], "MiniMax-H3-MAX")
|
||||
self.assertEqual(body["resolution"], "480P")
|
||||
|
||||
invalid_cases = [
|
||||
{"resolution": "2K", "duration": 5, "mode": MODE_TEXT, "ratio": "16:9"},
|
||||
{"resolution": "480P", "duration": 4, "mode": MODE_TEXT, "ratio": "16:9"},
|
||||
{
|
||||
"resolution": "768P",
|
||||
"duration": 5,
|
||||
"mode": MODE_REFERENCE,
|
||||
"reference_image_urls": ["https://cdn.example.com/reference.png"],
|
||||
},
|
||||
]
|
||||
for case in invalid_cases:
|
||||
with self.subTest(case=case), self.assertRaises(ValueError):
|
||||
build_request_body(prompt="x", model=MODEL_MAX_ID, **case)
|
||||
|
||||
def test_validation(self):
|
||||
with self.assertRaises(ValueError):
|
||||
build_request_body(
|
||||
prompt="x",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_TEXT,
|
||||
ratio="adaptive",
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
build_request_body(
|
||||
prompt="x" * 7001,
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_TEXT,
|
||||
ratio="16:9",
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
build_request_body(
|
||||
prompt="x",
|
||||
resolution="2K",
|
||||
duration=3,
|
||||
mode=MODE_TEXT,
|
||||
ratio="16:9",
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
build_request_body(
|
||||
prompt="x",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_REFERENCE,
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
build_request_body(
|
||||
prompt="x",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_FIRST_LAST,
|
||||
first_url="https://cdn.example.com/first.png",
|
||||
last_url="https://cdn.example.com/last.png",
|
||||
reference_image_urls=["https://cdn.example.com/reference.png"],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
build_request_body(
|
||||
prompt="x",
|
||||
resolution="2K",
|
||||
duration=5,
|
||||
mode=MODE_REFERENCE,
|
||||
reference_image_urls=[f"https://cdn.example.com/{i}.png" for i in range(10)],
|
||||
)
|
||||
|
||||
|
||||
class MiniMaxH3ClientTests(unittest.TestCase):
|
||||
def test_video_clients_share_2000_second_poll_deadline(self):
|
||||
self.assertEqual(POLL_DEADLINE_SECONDS, 2000)
|
||||
self.assertEqual(MiniMaxH3Client.POLL_DEADLINE_SECONDS, 2000)
|
||||
self.assertEqual(NewAPIVeoClient.POLL_DEADLINE_SECONDS, 2000)
|
||||
self.assertEqual(
|
||||
signature(NewAPIVeoClient.poll_video_status_async)
|
||||
.parameters["timeout"].default,
|
||||
2000,
|
||||
)
|
||||
self.assertEqual(
|
||||
signature(NewAPIVeoClient.generate_video_sync)
|
||||
.parameters["timeout"].default,
|
||||
2000,
|
||||
)
|
||||
|
||||
def test_public_id_precedence(self):
|
||||
self.assertEqual(
|
||||
extract_public_task_id({"id": "public-id", "task_id": "fallback-id"}),
|
||||
"public-id",
|
||||
)
|
||||
|
||||
def test_wrapped_success_snapshot(self):
|
||||
snapshot = parse_task_snapshot({
|
||||
"code": "success",
|
||||
"data": {
|
||||
"status": "SUCCESS",
|
||||
"progress": "100%",
|
||||
"result_url": "https://cdn.example.com/result.mp4",
|
||||
},
|
||||
})
|
||||
self.assertEqual(snapshot["status"], "SUCCESS")
|
||||
self.assertEqual(snapshot["progress"], 100)
|
||||
self.assertEqual(snapshot["result_url"], "https://cdn.example.com/result.mp4")
|
||||
|
||||
def test_http_200_failure_snapshot(self):
|
||||
snapshot = parse_task_snapshot({
|
||||
"code": "success",
|
||||
"data": {
|
||||
"status": "FAILURE",
|
||||
"fail_reason": "上游拒绝",
|
||||
},
|
||||
})
|
||||
self.assertEqual(snapshot["status"], "FAILURE")
|
||||
self.assertEqual(snapshot["fail_reason"], "上游拒绝")
|
||||
|
||||
def test_official_v2_success_snapshot(self):
|
||||
snapshot = parse_task_snapshot({
|
||||
"task": {
|
||||
"id": "424010985738629",
|
||||
"status": "succeeded",
|
||||
"content": {"url": "https://cdn.example.com/official.mp4"},
|
||||
}
|
||||
})
|
||||
self.assertEqual(snapshot["status"], "SUCCEEDED")
|
||||
self.assertEqual(snapshot["result_url"], "https://cdn.example.com/official.mp4")
|
||||
|
||||
def test_official_v2_failure_snapshot(self):
|
||||
snapshot = parse_task_snapshot({
|
||||
"task": {
|
||||
"status": "failed",
|
||||
"error": {"code": "1026", "message": "sensitive content"},
|
||||
}
|
||||
})
|
||||
self.assertEqual(snapshot["status"], "FAILED")
|
||||
self.assertIn("1026", snapshot["fail_reason"])
|
||||
|
||||
def test_official_status_sets(self):
|
||||
self.assertIn("RUNNING", PENDING_STATUSES)
|
||||
self.assertIn("UNKNOWN", PENDING_STATUSES)
|
||||
self.assertIn("SUCCEEDED", SUCCESS_STATUSES)
|
||||
self.assertIn("CANCELLED", FAILURE_STATUSES)
|
||||
|
||||
def test_latest_completed_and_failed_response_shapes(self):
|
||||
completed = parse_task_snapshot({
|
||||
"id": "task-public-id",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"metadata": {"url": "https://cdn.example.com/metadata-result.mp4"},
|
||||
})
|
||||
self.assertEqual(completed["status"], "COMPLETED")
|
||||
self.assertEqual(
|
||||
completed["result_url"],
|
||||
"https://cdn.example.com/metadata-result.mp4",
|
||||
)
|
||||
|
||||
failed = parse_task_snapshot({
|
||||
"task_id": "task-public-id",
|
||||
"status": "failed",
|
||||
"error": {"code": "upstream_rejected", "message": "内容被拒绝"},
|
||||
})
|
||||
self.assertEqual(failed["status"], "FAILED")
|
||||
self.assertIn("upstream_rejected", failed["fail_reason"])
|
||||
|
||||
def test_headers_do_not_include_management_user_header(self):
|
||||
headers = MiniMaxH3Client(
|
||||
base_url="https://new-api.example.com/",
|
||||
api_key="test-token",
|
||||
)._headers()
|
||||
self.assertEqual(headers["Authorization"], "Bearer test-token")
|
||||
self.assertNotIn("New-Api-User", headers)
|
||||
|
||||
def test_v3_schema_and_registration(self):
|
||||
schema = MiniMaxH3Video.define_schema()
|
||||
schema.validate()
|
||||
self.assertIsNotNone(schema)
|
||||
self.assertEqual(
|
||||
[item.id for item in schema.inputs[-2:]],
|
||||
["模型", "seed"],
|
||||
)
|
||||
seed_input = schema.inputs[-1]
|
||||
self.assertEqual(seed_input.default, 0)
|
||||
self.assertEqual(seed_input.min, 0)
|
||||
self.assertEqual(seed_input.max, MAX_SEED)
|
||||
from comfyui_o1key import NODE_CLASS_MAPPINGS
|
||||
|
||||
self.assertIs(NODE_CLASS_MAPPINGS["MiniMaxH3Video"], MiniMaxH3Video)
|
||||
|
||||
@patch("comfy.utils.ProgressBar")
|
||||
def test_node_progress_mirrors_gateway_percentage_without_regressing(self, progress_cls):
|
||||
progress_bar = progress_cls.return_value
|
||||
on_stage, on_progress = _make_progress_callbacks()
|
||||
|
||||
on_stage("submitting")
|
||||
on_stage("submitted:task-public-id")
|
||||
on_progress(20)
|
||||
on_progress(20)
|
||||
on_progress(10)
|
||||
on_progress(50)
|
||||
on_progress(100)
|
||||
on_stage("downloading")
|
||||
on_stage("done")
|
||||
|
||||
self.assertEqual(
|
||||
progress_bar.update_absolute.call_args_list,
|
||||
[call(0, 100), call(20, 100), call(50, 100), call(100, 100)],
|
||||
)
|
||||
|
||||
|
||||
class MiniMaxH3PollingTests(unittest.IsolatedAsyncioTestCase):
|
||||
@staticmethod
|
||||
def _response(payload):
|
||||
response = AsyncMock()
|
||||
response.text = AsyncMock(return_value=json.dumps(payload))
|
||||
return response
|
||||
|
||||
async def test_documented_unknown_continues_until_gateway_syncs(self):
|
||||
request = AsyncMock(side_effect=[
|
||||
self._response({
|
||||
"id": "task-public-id",
|
||||
"status": "unknown",
|
||||
"progress": 0,
|
||||
}),
|
||||
self._response({
|
||||
"data": {
|
||||
"status": "QUEUED",
|
||||
"progress": 1,
|
||||
"result_url": "",
|
||||
}
|
||||
}),
|
||||
self._response({
|
||||
"data": {
|
||||
"status": "SUCCESS",
|
||||
"progress": 100,
|
||||
"result_url": "https://cdn.example.com/result.mp4",
|
||||
}
|
||||
}),
|
||||
])
|
||||
sleep = AsyncMock()
|
||||
client = MiniMaxH3Client(
|
||||
base_url="https://new-api.example.com",
|
||||
api_key="test-token",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"comfyui_o1key.clients.minimax_h3_client.async_request_with_retry",
|
||||
request,
|
||||
),
|
||||
patch(
|
||||
"comfyui_o1key.clients.minimax_h3_client.interruptible_sleep",
|
||||
sleep,
|
||||
),
|
||||
):
|
||||
result_url = await client.poll_async("task-public-id", object())
|
||||
|
||||
self.assertEqual(result_url, "https://cdn.example.com/result.mp4")
|
||||
self.assertEqual(request.await_count, 3)
|
||||
self.assertEqual(sleep.await_args_list, [call(10.0), call(10.0)])
|
||||
self.assertEqual(
|
||||
request.await_args_list[0].args[2],
|
||||
"https://new-api.example.com/v1/videos/task-public-id",
|
||||
)
|
||||
|
||||
async def test_generate_downloads_completed_result_without_api_headers(self):
|
||||
session = object()
|
||||
session_context = MagicMock()
|
||||
session_context.__aenter__ = AsyncMock(return_value=session)
|
||||
session_context.__aexit__ = AsyncMock(return_value=False)
|
||||
submit = AsyncMock(return_value="task-public-id")
|
||||
poll = AsyncMock(return_value="https://cdn.example.com/result.mp4")
|
||||
download = AsyncMock(return_value="result.mp4")
|
||||
stages = []
|
||||
client = MiniMaxH3Client(
|
||||
base_url="https://new-api.example.com",
|
||||
api_key="test-token",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"comfyui_o1key.clients.minimax_h3_client.aiohttp.TCPConnector",
|
||||
return_value=object(),
|
||||
),
|
||||
patch(
|
||||
"comfyui_o1key.clients.minimax_h3_client.aiohttp.ClientSession",
|
||||
return_value=session_context,
|
||||
),
|
||||
patch.object(client, "submit_async", submit),
|
||||
patch.object(client, "poll_async", poll),
|
||||
patch(
|
||||
"comfyui_o1key.clients.minimax_h3_client.download_video_to_file",
|
||||
download,
|
||||
),
|
||||
):
|
||||
result = await client.generate_async(
|
||||
body={"model": "MiniMax-H3"},
|
||||
save_path="result.mp4",
|
||||
on_stage=stages.append,
|
||||
)
|
||||
|
||||
self.assertEqual(result, ("result.mp4", "task-public-id"))
|
||||
self.assertEqual(
|
||||
stages,
|
||||
["submitting", "submitted:task-public-id", "downloading", "done"],
|
||||
)
|
||||
download.assert_awaited_once_with(
|
||||
session,
|
||||
"https://cdn.example.com/result.mp4",
|
||||
"result.mp4",
|
||||
label="MiniMax H3 task-public-id",
|
||||
)
|
||||
|
||||
|
||||
class MiniMaxH3MediaValidationTests(unittest.TestCase):
|
||||
def test_image_limits(self):
|
||||
info = validate_image(Image.new("RGB", (256, 256)), "测试图片")
|
||||
self.assertEqual(info["width"], 256)
|
||||
with self.assertRaises(ValueError):
|
||||
validate_image(Image.new("RGB", (255, 256)), "过小图片")
|
||||
|
||||
def test_audio_duration(self):
|
||||
audio = {
|
||||
"waveform": torch.zeros((1, 1, 48000 * 2)),
|
||||
"sample_rate": 48000,
|
||||
}
|
||||
info = inspect_audio(audio)
|
||||
self.assertEqual(info["duration"], 2.0)
|
||||
with self.assertRaises(ValueError):
|
||||
inspect_audio({
|
||||
"waveform": torch.zeros((1, 1, 48000)),
|
||||
"sample_rate": 48000,
|
||||
})
|
||||
with self.assertRaises(ValueError):
|
||||
validate_reference_audios([
|
||||
{"waveform": torch.zeros((1, 1, 48000 * 8)), "sample_rate": 48000},
|
||||
{"waveform": torch.zeros((1, 1, 48000 * 8)), "sample_rate": 48000},
|
||||
])
|
||||
|
||||
def test_video_metadata_limits(self):
|
||||
valid = {
|
||||
"size": 1024,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"duration": 5.0,
|
||||
"fps": 24.0,
|
||||
"video_codec": "h264",
|
||||
"audio_codecs": {"aac"},
|
||||
"format_names": {"mov", "mp4"},
|
||||
}
|
||||
validate_video_info(valid)
|
||||
# The current API contract constrains the MP4/MOV container, duration,
|
||||
# and frame rate, but does not impose a client-side codec allowlist.
|
||||
validate_video_info(dict(valid, video_codec="vp9", audio_codecs={"opus"}))
|
||||
invalid = dict(valid, format_names={"matroska"})
|
||||
with self.assertRaises(ValueError):
|
||||
validate_video_info(invalid)
|
||||
with patch(
|
||||
"comfyui_o1key.utils.minimax_h3_media.probe_video",
|
||||
side_effect=[dict(valid, duration=8.0), dict(valid, duration=8.0)],
|
||||
):
|
||||
with self.assertRaises(ValueError):
|
||||
validate_reference_videos([object(), object()])
|
||||
|
||||
def test_probe_real_mp4(self):
|
||||
fd, path = tempfile.mkstemp(suffix=".mp4", prefix="minimax_h3_probe_")
|
||||
os.close(fd)
|
||||
try:
|
||||
container = av.open(path, mode="w")
|
||||
stream = container.add_stream("libx264", rate=24)
|
||||
stream.width = 256
|
||||
stream.height = 256
|
||||
stream.pix_fmt = "yuv420p"
|
||||
frame_data = np.zeros((256, 256, 3), dtype=np.uint8)
|
||||
for _ in range(72):
|
||||
frame = av.VideoFrame.from_ndarray(frame_data, format="rgb24")
|
||||
for packet in stream.encode(frame):
|
||||
container.mux(packet)
|
||||
for packet in stream.encode():
|
||||
container.mux(packet)
|
||||
container.close()
|
||||
|
||||
info = probe_video(path)
|
||||
validate_video_info(info)
|
||||
self.assertEqual(info["video_codec"], "h264")
|
||||
self.assertGreaterEqual(info["duration"], 2.0)
|
||||
finally:
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user