feat: add API for adding watermark image to video, with tests and read-me file

This commit is contained in:
Jerry Tian 2025-04-06 22:18:16 -04:00
parent 6694cf0170
commit 547c79123d
9 changed files with 605 additions and 51 deletions

View File

@ -0,0 +1,85 @@
#!/usr/bin/env python3
import requests
import json
import time
import sys
# Base URL for the API
BASE_URL = "http://localhost:4200/api/script"
# Arguments to pass to the script
ARG1 = "hello"
ARG2 = "world"
def main():
print("Starting script execution...")
# Start script execution and get token
try:
response = requests.post(
BASE_URL,
json={
"scriptPath": "/tmp/api_scripts/hello_test.sh",
"args": [ARG1, ARG2]
},
headers={"Content-Type": "application/json"}
)
response.raise_for_status() # Raise exception for non-200 status codes
token_data = response.json()
token = token_data.get("token")
if not token:
print(f"Failed to get token. Response: {response.text}")
sys.exit(1)
print(f"Received token: {token}")
print("Polling for results...")
except requests.exceptions.RequestException as e:
print(f"Error starting script execution: {e}")
sys.exit(1)
# Poll for results with timeout
MAX_ATTEMPTS = 30
SLEEP_SECONDS = 1
status = "pending"
for attempt in range(1, MAX_ATTEMPTS + 1):
if status not in ["pending", "running"]:
break
print(f"Polling attempt {attempt} of {MAX_ATTEMPTS}...")
try:
result_response = requests.get(f"{BASE_URL}?token={token}")
result_response.raise_for_status()
result_data = result_response.json()
status = result_data.get("status")
if status in ["pending", "running"]:
print(f"Script is still {status}. Waiting {SLEEP_SECONDS}s...")
time.sleep(SLEEP_SECONDS)
else:
break
except requests.exceptions.RequestException as e:
print(f"Error polling for results: {e}")
sys.exit(1)
if status in ["pending", "running"]:
print("Timeout waiting for script to complete")
sys.exit(1)
# Print results
print(f"Script execution completed with status: {status}")
print(f"Result: {json.dumps(result_data, indent=2)}")
# Pretty print stdout if available
if result_data.get("result", {}).get("stdout"):
stdout = result_data["result"]["stdout"]
print("\nScript output:")
print(stdout)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,131 @@
# Video Watermark API Usage Guide
## Overview
The Video Watermark API allows you to add watermarks to video files asynchronously. The process involves two steps:
1. Submit a watermark job and receive a token
2. Use the token to check the job status and get the result
## Base URL
```
https://animator-gg-api.ca2324.servep2p.com:8443
```
## Step 1: Submit a Video Watermark Job
**Request:**
```bash
curl -X POST "https://animator-gg-api.ca2324.servep2p.com:8443/api/script" \
-H "Content-Type: application/json" \
-d '{
"scriptPath": "/tmp/api_scripts/video_add_watermark.sh",
"args": [
"https://example.com/input.mp4",
"https://example.com/watermark.png",
"30",
"30"
]
}'
```
**Parameters:**
- `scriptPath`: Must be video_add_watermark.sh
- `args`: Array containing:
- Input video URL (required)
- Watermark image URL (required)
- X position in pixels (required)
- Y position in pixels (required)
- Width for watermark resize (optional, 0 for no resize)
- Height for watermark resize (optional, 0 for no resize)
**Response:**
```json
{
"token": "550e8400-e29b-41d4-a716-446655440000"
}
```
## Step 2: Check Job Status and Get Result
**Request:**
```bash
curl -X GET "https://animator-gg-api.ca2324.servep2p.com:8443/api/script?token=550e8400-e29b-41d4-a716-446655440000"
```
**Response:**
```json
{
"status": "completed",
"result": {
"success": true,
"stdout": "{\n \"input_video_url\": \"https://example.com/input.mp4\",\n \"watermark_png_url\": \"https://example.com/watermark.png\", \n \"pos_x\": 30,\n \"pos_y\": 30,\n \"size_w\": 0,\n \"size_h\": 0,\n \"output_file_name\": \"/var/www/html/watermarked_videos/bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\",\n \"output_file_url\": \"https://example.com/watermarked_videos/bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\"\n}",
"stderr": "..."
},
"timestamp": 1743991194237
}
```
When the job is complete, the `stdout` field will contain a JSON string with the output video URL. You'll need to parse this string to get the actual URL.
## Example: Complete Process
1. **Submit the job:**
```bash
TOKEN=$(curl -s -X POST "https://animator-gg-api.ca2324.servep2p.com:8443/api/script" \
-H "Content-Type: application/json" \
-d '{
"scriptPath": "/tmp/api_scripts/video_add_watermark.sh",
"args": [
"https://ca2324.ddns.net:8443/jerry/_seed847064922.mp4",
"https://ca2324.ddns.net:8443/jerry/0a97d84a70fec3991cf05ee99ceb6469.png",
"30",
"30"
]
}' | jq -r '.token')
echo "Job submitted with token: $TOKEN"
```
2. **Check status until complete:**
```bash
while true; do
RESULT=$(curl -s -X GET "https://animator-gg-api.ca2324.servep2p.com:8443/api/script?token=$TOKEN")
STATUS=$(echo $RESULT | jq -r '.status')
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
break
fi
sleep 2
done
# Extract the watermarked video URL
if [ "$STATUS" = "completed" ]; then
OUTPUT_JSON=$(echo $RESULT | jq -r '.result.stdout')
OUTPUT_URL=$(echo $OUTPUT_JSON | jq -r '.output_file_url')
echo "Watermarked video URL: $OUTPUT_URL"
fi
```
## Status Codes
- `pending`: Job is waiting in the queue
- `running`: Job is currently processing
- `completed`: Job finished successfully
- `failed`: Job failed to complete
## Notes
- Results are stored for 24 hours after completion
- Processing large videos may take several minutes
- The API is rate-limited to 2 concurrent jobs

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
#bash video_add_watermark.sh "$VID" "$PNG" 30 30
import requests
import json
import time
import sys
# Base URL for the API
BASE_URL = "https://animator-gg-api.ca2324.servep2p.com:8443/api/script"
# Arguments to pass to the script
VID="https://ca2324.ddns.net:8443/jerry/_seed847064922.mp4"
PNG="https://ca2324.ddns.net:8443/jerry/0a97d84a70fec3991cf05ee99ceb6469.png"
SAMPLE_OUPUT = """
{
"status": "completed",
"result": {
"success": true,
"stdout": "{\n \"input_video_url\": \"https://ca2324.ddns.net:8443/jerry/_seed847064922.mp4\",\n \"watermark_png_url\": \"https://ca2324.ddns.net:8443/jerry/0a97d84a70fec3991cf05ee99ceb6469.png\", \n \"pos_x\": 30,\n \"pos_y\": 30,\n \"size_w\": 0,\n \"size_h\": 0,\n \"output_file_name\": \"/var/www/html/watermarked_videos//bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\",\n \"output_file_url\": \"https://ca2324.ddns.net:8443/watermarked_videos/bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\"\n}",
"stderr": "x265 [info]: HEVC encoder version 4.1+1-1d117be\nx265 [info]: build info [Linux][GCC 11.4.0][64 bit] 8bit+10bit+12bit\nx265 [info]: using cpu capabilities: MMX2 SSE2Fast LZCNT SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2\nx265 [info]: Main profile, Level-4 (Main tier)\nx265 [info]: Thread pool created using 56 threads\nx265 [info]: Slices : 1\nx265 [info]: frame threads / pool features : 5 / wpp(25 rows)\nx265 [info]: Coding QT: max CU size, min CU size : 64 / 8\nx265 [info]: Residual QT: max TU size, max depth : 32 / 1 inter / 1 intra\nx265 [info]: ME / range / subpel / merge : hex / 57 / 1 / 2\nx265 [info]: Keyframe min / max / scenecut / bias : 24 / 250 / 40 / 5.00 \nx265 [info]: Lookahead / bframes / badapt : 15 / 4 / 0\nx265 [info]: b-pyramid / weightp / weightb : 1 / 1 / 0\nx265 [info]: References / ref-limit cu / depth : 2 / on / on\nx265 [info]: AQ: mode / str / qg-size / cu-tree : 2 / 1.0 / 32 / 1\nx265 [info]: Rate Control / qCompress : CRF-24.0 / 0.60\nx265 [info]: tools: rd=2 psy-rd=2.00 early-skip rskip mode=1 signhide tmvp\nx265 [info]: tools: fast-intra strong-intra-smoothing lslices=8 deblock sao\nx265 [info]: frame I: 2, Avg QP:24.88 kb/s: 14848.22\nx265 [info]: frame P: 95, Avg QP:24.31 kb/s: 8688.46 \nx265 [info]: frame B: 384, Avg QP:29.93 kb/s: 2459.11 \nx265 [info]: Weighted P-Frames: Y:8.4% UV:5.3%\n\nencoded 481 frames in 8.94s (53.79 fps), 3740.95 kb/s, Avg QP:28.80"
},
"timestamp": 1743991194237
}
"""
def main():
print("Starting script execution...")
# Start script execution and get token
try:
response = requests.post(
BASE_URL,
json={
"scriptPath": "/tmp/api_scripts/video_add_watermark.sh",
"args": [VID, PNG, 30, 30]
},
headers={"Content-Type": "application/json"}
)
response.raise_for_status() # Raise exception for non-200 status codes
token_data = response.json()
token = token_data.get("token")
if not token:
print(f"Failed to get token. Response: {response.text}")
sys.exit(1)
print(f"Received token: {token}")
print("Polling for results...")
except requests.exceptions.RequestException as e:
print(f"Error starting script execution: {e}")
sys.exit(1)
# Poll for results with timeout
MAX_ATTEMPTS = 30
SLEEP_SECONDS = 1
status = "pending"
for attempt in range(1, MAX_ATTEMPTS + 1):
if status not in ["pending", "running"]:
break
print(f"Polling attempt {attempt} of {MAX_ATTEMPTS}...")
try:
result_response = requests.get(f"{BASE_URL}?token={token}")
result_response.raise_for_status()
result_data = result_response.json()
status = result_data.get("status")
if status in ["pending", "running"]:
print(f"Script is still {status}. Waiting {SLEEP_SECONDS}s...")
time.sleep(SLEEP_SECONDS)
else:
break
except requests.exceptions.RequestException as e:
print(f"Error polling for results: {e}")
sys.exit(1)
if status in ["pending", "running"]:
print("Timeout waiting for script to complete")
sys.exit(1)
# Print results
print(f"Script execution completed with status: {status}")
print(f"Result: {json.dumps(result_data, indent=2)}")
# Pretty print stdout if available
if result_data.get("result", {}).get("stdout"):
stdout = result_data["result"]["stdout"]
print("\nScript output:")
print(stdout)
if __name__ == "__main__":
main()

View File

@ -1,4 +1,112 @@
#!/bin/bash #!/bin/bash
echo "Script executed with args: $1 $2"
echo "Current time: $(date)" output_folder="/var/www/html/watermarked_videos/"
echo "Current directory: $(pwd)" output_base_url="https://ca2324.ddns.net:8443/watermarked_videos/"
# Input parameters
input_video_url="$1"
watermark_png_url="$2"
pos_x="${3:-10}" # Default to 10 if not provided
pos_y="${4:-10}" # Default to 10 if not provided
size_w="${5:-0}" # Default to 0 (keep aspect ratio) if not provided
size_h="${6:-0}" # Default to 0 (keep aspect ratio) if not provided
# Validate required inputs
if [ -z "$input_video_url" ] || [ -z "$watermark_png_url" ]; then
echo "Error: Missing required parameters" >&2
echo "Usage: $0 input_video_url watermark_png_url [pos_x] [pos_y] [size_w] [size_h]" >&2
exit 1
fi
# Create temp directory
temp_dir="/tmp/watermark_$$"
mkdir -p "$temp_dir" || {
echo "Error: Failed to create temporary directory" >&2
exit 1
}
# Download input video
video_filename=$(basename "$input_video_url")
local_video_path="$temp_dir/$video_filename"
if ! curl -s -L -o "$local_video_path" "$input_video_url"; then
echo "Error: Failed to download input video from $input_video_url" >&2
rm -rf "$temp_dir"
exit 1
fi
# Download watermark image
watermark_filename=$(basename "$watermark_png_url")
local_watermark_path="$temp_dir/$watermark_filename"
if ! curl -s -L -o "$local_watermark_path" "$watermark_png_url"; then
echo "Error: Failed to download watermark image from $watermark_png_url" >&2
rm -rf "$temp_dir"
exit 1
fi
# Validate downloaded files
if [ ! -f "$local_video_path" ]; then
echo "Error: Downloaded video file not found" >&2
rm -rf "$temp_dir"
exit 1
fi
if [ ! -f "$local_watermark_path" ]; then
echo "Error: Downloaded watermark file not found" >&2
rm -rf "$temp_dir"
exit 1
fi
# Generate a UUID-style output filename
output_uuid=$(uuidgen)
output_file="$temp_dir/$output_uuid.mp4"
# Prepare ffmpeg filter for watermark
if [ "$size_w" -eq 0 ] && [ "$size_h" -eq 0 ]; then
# No resizing
filter_complex="overlay=$pos_x:$pos_y"
else
# Resize watermark before overlay
filter_complex="overlay=$pos_x:$pos_y:scale=$size_w:$size_h"
fi
# Add watermark using ffmpeg and encode to HEVC (H.265)
if ! ffmpeg -y -loglevel error -i "$local_video_path" -i "$local_watermark_path" \
-filter_complex "$filter_complex" \
-c:v libx265 -tag:v hvc1 -crf 24 -preset veryfast \
-c:a copy \
-movflags +faststart \
"${output_file}" 1>&2 ; then
echo "Error: Failed to add watermark to video" >&2
rm -rf "$temp_dir"
exit 1
fi
# Move output file to the output folder
if ! mv "$output_file" "$output_folder"; then
echo "Error: Failed to move output file to $output_folder" >&2
rm -rf "$temp_dir"
exit 1
fi
# Clean up temporary files
rm -rf "$temp_dir"
# Generate the final output file path
output_file="$output_folder/$output_uuid.mp4"
# Generate the final output URL
output_file_url="${output_base_url}${output_uuid}.mp4"
# Return JSON with parameters and output filename
cat << EOF
{
"input_video_url": "$input_video_url",
"watermark_png_url": "$watermark_png_url",
"pos_x": $pos_x,
"pos_y": $pos_y,
"size_w": $size_w,
"size_h": $size_h,
"output_file_name": "$output_file",
"output_file_url": "$output_file_url"
}
EOF
# Note: We're not deleting the temp directory so that the output file remains available
exit 0

View File

@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { getQueueStats } from "../../../../services/bashExecService";
// Endpoint to get queue statistics
export async function GET() {
try {
const stats = getQueueStats();
return NextResponse.json(stats);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -1,9 +1,10 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { import {
executeScript, executeScript,
getQueueStats, getScriptExecution,
} from "../../../services/bashExecService"; } from "../../../services/bashExecService";
// Modified POST method that returns a token instead of waiting for execution result
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json(); const body = await request.json();
@ -16,22 +17,33 @@ export async function POST(request: NextRequest) {
); );
} }
const result = await executeScript({ const token = executeScript({
scriptPath, scriptPath,
args, args,
}); });
return NextResponse.json(result); return NextResponse.json({ token });
} catch (error: any) { } catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });
} }
} }
// Endpoint to get queue statistics // API to query execution result by token from path parameter
export async function GET() { export async function GET(request: NextRequest) {
try { try {
const stats = getQueueStats(); // Get token from the search params in the URL
return NextResponse.json(stats); const { searchParams } = new URL(request.url);
const token = searchParams.get("token");
if (!token) {
return NextResponse.json(
{ error: "Token is required as a query parameter" },
{ status: 400 },
);
}
const execution = getScriptExecution(token);
return NextResponse.json(execution);
} catch (error: any) { } catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });
} }

View File

@ -1,8 +1,13 @@
import { execa } from "execa"; import { execa } from "execa";
import PQueue from "p-queue"; import PQueue from "p-queue";
import { v4 as uuidv4 } from "uuid"; // Make sure to install this package
// Configure the queue with a concurrency limit // Configure the queue with a concurrency limit
const scriptQueue = new PQueue({ concurrency: 5 }); const scriptQueue = new PQueue({ concurrency: 2 });
// Cleanup interval (24 hours in ms)
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000;
const RESULT_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in ms
export type ScriptParams = { export type ScriptParams = {
scriptPath: string; scriptPath: string;
@ -17,37 +22,76 @@ export type ScriptResult = {
error?: string; error?: string;
}; };
export type ScriptQueueState = {
status: "pending" | "running" | "completed" | "failed";
result?: ScriptResult;
timestamp: number;
};
// Store execution results by token
const executionResults = new Map<string, ScriptQueueState>();
/** /**
* Executes a bash script with queue management * Clean up expired execution results
*/ */
export async function executeScript( function cleanupExpiredResults() {
params: ScriptParams, const now = Date.now();
): Promise<ScriptResult> { for (const [token, data] of executionResults.entries()) {
// Add the script execution to the queue if (now - data.timestamp > RESULT_EXPIRY) {
executionResults.delete(token);
// hardcoded the allowed script path for security reasons }
var allowedScriptPaths = [
"/tmp/api_scripts/hello_test.sh",
"/tmp/api_scripts/video_add_watermark.sh",
];
if (!allowedScriptPaths.includes(params.scriptPath)) {
return {
success: false,
stdout: "",
stderr: "",
error: "Script path is not allowed",
};
} }
}
const result = await scriptQueue.add(async () => { // Set up periodic cleanup
setInterval(cleanupExpiredResults, CLEANUP_INTERVAL);
/**
* Executes a bash script with queue management and returns a token immediately
*/
export function executeScript(params: ScriptParams): string {
const token = uuidv4();
// Initialize result as pending with current timestamp
executionResults.set(token, {
status: "pending",
timestamp: Date.now(),
});
// Add the script execution to the queue
scriptQueue.add(async () => {
try { try {
// hardcoded the allowed script path for security reasons
var allowedScriptPaths = [
"/tmp/api_scripts/hello_test.sh",
"/tmp/api_scripts/video_add_watermark.sh",
];
if (!allowedScriptPaths.includes(params.scriptPath)) {
executionResults.set(token, {
status: "failed",
result: {
success: false,
stdout: "",
stderr: "",
error: "Script path is not allowed",
},
timestamp: Date.now(),
});
return;
}
// Update status to running
executionResults.set(token, {
status: "running",
timestamp: Date.now(),
});
// Execute the bash script // Execute the bash script
const { stdout, stderr } = await execa( const { stdout, stderr } = await execa(
"bash", "bash",
[params.scriptPath, ...(params.args || [])], [params.scriptPath, ...(params.args || [])],
{ {
buffer: true, // Changed to true to ensure we get the complete output buffer: true, // Ensure we get the complete output
}, },
); );
@ -56,23 +100,60 @@ export async function executeScript(
params.onProgress(stdout.toString()); params.onProgress(stdout.toString());
} }
return { // Store successful result
success: true, executionResults.set(token, {
stdout: stdout ? stdout.toString() : "", status: "completed",
stderr: stderr ? stderr.toString() : "", result: {
}; success: true,
stdout: stdout ? stdout.toString() : "",
stderr: stderr ? stderr.toString() : "",
},
timestamp: Date.now(),
});
} catch (error: any) { } catch (error: any) {
// Handle script execution errors // Handle script execution errors
return { executionResults.set(token, {
success: false, status: "failed",
stdout: error.stdout ? error.stdout.toString() : "", result: {
stderr: error.stderr ? error.stderr.toString() : "", success: false,
error: error.message || "Failed to execute script", stdout: error.stdout ? error.stdout.toString() : "",
}; stderr: error.stderr ? error.stderr.toString() : "",
error: error.message || "Failed to execute script",
},
timestamp: Date.now(),
});
} }
}); });
return result as ScriptResult; return token;
}
/**
* Query the status and result of a script execution
* @param token The token returned by executeScript
* @returns An object containing execution status and result (if available)
*/
export function getScriptExecution(token: string): ScriptQueueState {
const execution = executionResults.get(token);
if (!execution) {
return {
status: "failed",
result: {
success: false,
stdout: "",
stderr: "",
error: "Invalid execution token or result expired",
},
timestamp: Date.now(),
};
}
return {
status: execution.status,
result: execution.result,
timestamp: Date.now(),
};
} }
// Get current queue size and pending tasks // Get current queue size and pending tasks
@ -81,6 +162,7 @@ export function getQueueStats() {
size: scriptQueue.size, size: scriptQueue.size,
pending: scriptQueue.pending, pending: scriptQueue.pending,
isPaused: scriptQueue.isPaused, isPaused: scriptQueue.isPaused,
activeExecutions: executionResults.size,
}; };
} }

34
package-lock.json generated
View File

@ -42,7 +42,8 @@
"regenerator-runtime": "0.13.7", "regenerator-runtime": "0.13.7",
"robot3": "^0.4.1", "robot3": "^0.4.1",
"ts-morph": "^17.0.1", "ts-morph": "^17.0.1",
"tslib": "^2.3.0" "tslib": "^2.3.0",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.0.0", "@commitlint/cli": "^17.0.0",
@ -3014,6 +3015,15 @@
"node": ">= 0.12" "node": ">= 0.12"
} }
}, },
"node_modules/@cypress/request/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@cypress/xvfb": { "node_modules/@cypress/xvfb": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
@ -30831,6 +30841,15 @@
"websocket-driver": "^0.7.4" "websocket-driver": "^0.7.4"
} }
}, },
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -33282,12 +33301,15 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"dev": true, "funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
}, },
"node_modules/uvu": { "node_modules/uvu": {

View File

@ -62,7 +62,8 @@
"regenerator-runtime": "0.13.7", "regenerator-runtime": "0.13.7",
"robot3": "^0.4.1", "robot3": "^0.4.1",
"ts-morph": "^17.0.1", "ts-morph": "^17.0.1",
"tslib": "^2.3.0" "tslib": "^2.3.0",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.0.0", "@commitlint/cli": "^17.0.0",