Compare commits
37 Commits
v1.24.0
...
js/async_n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ada4f1d533 | ||
|
|
aae20d9417 | ||
|
|
1200c07fcd | ||
|
|
be7edab141 | ||
|
|
f3168aac89 | ||
|
|
2f3c762e85 | ||
|
|
8b8caa4b29 | ||
|
|
a70d69cbd2 | ||
|
|
9f4abbc3af | ||
|
|
01c735d943 | ||
|
|
f0bc4c6959 | ||
|
|
0887bb6654 | ||
|
|
19eaf6ecdc | ||
|
|
054077c445 | ||
|
|
ef9b625208 | ||
|
|
688193ad9a | ||
|
|
5c119fcbda | ||
|
|
998abbbdbd | ||
|
|
696c8720b6 | ||
|
|
80e5cf1b9d | ||
|
|
7cf5d1e86b | ||
|
|
868e047272 | ||
|
|
abb82e5fd1 | ||
|
|
ab43b5e421 | ||
|
|
43f73f8856 | ||
|
|
0b5ade3a3b | ||
|
|
eb63b5c536 | ||
|
|
e6d78ab22c | ||
|
|
bc4753e119 | ||
|
|
db70265e16 | ||
|
|
c8137f7f98 | ||
|
|
132a9dbb5f | ||
|
|
f076a1c422 | ||
|
|
f6c65d3fe7 | ||
|
|
c8371d6089 | ||
|
|
a70a81c9ae | ||
|
|
aa5fa81824 |
708
.claude/commands/comprehensive-pr-review.md
Normal file
@@ -0,0 +1,708 @@
|
||||
# Comprehensive PR Review for ComfyUI Frontend
|
||||
|
||||
<task>
|
||||
You are performing a comprehensive code review for PR #$1 in the ComfyUI frontend repository. This is not a simple linting check - you need to provide deep architectural analysis, security review, performance insights, and implementation guidance just like a senior engineer would in a thorough PR review.
|
||||
|
||||
Your review should cover:
|
||||
1. Architecture and design patterns
|
||||
2. Security vulnerabilities and risks
|
||||
3. Performance implications
|
||||
4. Code quality and maintainability
|
||||
5. Integration with existing systems
|
||||
6. Best practices and conventions
|
||||
7. Testing considerations
|
||||
8. Documentation needs
|
||||
</task>
|
||||
|
||||
Arguments: PR number passed via PR_NUMBER environment variable
|
||||
|
||||
## Phase 0: Initialize Variables and Helper Functions
|
||||
|
||||
```bash
|
||||
# Validate PR_NUMBER first thing
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "Error: PR_NUMBER environment variable is not set"
|
||||
echo "Usage: PR_NUMBER=<number> claude run /comprehensive-pr-review"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Initialize all counters at the start
|
||||
CRITICAL_COUNT=0
|
||||
HIGH_COUNT=0
|
||||
MEDIUM_COUNT=0
|
||||
LOW_COUNT=0
|
||||
ARCHITECTURE_ISSUES=0
|
||||
SECURITY_ISSUES=0
|
||||
PERFORMANCE_ISSUES=0
|
||||
QUALITY_ISSUES=0
|
||||
|
||||
# Helper function for posting review comments
|
||||
post_review_comment() {
|
||||
local file_path=$1
|
||||
local line_number=$2
|
||||
local severity=$3 # critical/high/medium/low
|
||||
local category=$4 # architecture/security/performance/quality
|
||||
local issue=$5
|
||||
local context=$6
|
||||
local suggestion=$7
|
||||
|
||||
# Update counters
|
||||
case $severity in
|
||||
"critical") ((CRITICAL_COUNT++)) ;;
|
||||
"high") ((HIGH_COUNT++)) ;;
|
||||
"medium") ((MEDIUM_COUNT++)) ;;
|
||||
"low") ((LOW_COUNT++)) ;;
|
||||
esac
|
||||
|
||||
case $category in
|
||||
"architecture") ((ARCHITECTURE_ISSUES++)) ;;
|
||||
"security") ((SECURITY_ISSUES++)) ;;
|
||||
"performance") ((PERFORMANCE_ISSUES++)) ;;
|
||||
"quality") ((QUALITY_ISSUES++)) ;;
|
||||
esac
|
||||
|
||||
# Post inline comment via GitHub CLI
|
||||
local comment="${issue}\n${context}\n${suggestion}"
|
||||
gh pr review $PR_NUMBER --comment --body "$comment" -F - <<< "$comment"
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 1: Environment Setup and PR Context
|
||||
|
||||
```bash
|
||||
# Pre-flight checks
|
||||
check_prerequisites() {
|
||||
# Check gh CLI is available
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# In GitHub Actions, auth is handled via GITHUB_TOKEN
|
||||
if [ -n "$GITHUB_ACTIONS" ] && [ -z "$GITHUB_TOKEN" ]; then
|
||||
echo "Error: GITHUB_TOKEN is not set in GitHub Actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we're authenticated
|
||||
if ! gh auth status &> /dev/null; then
|
||||
echo "Error: Not authenticated with GitHub. Run 'gh auth login'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set repository if not already set
|
||||
if [ -z "$REPOSITORY" ]; then
|
||||
REPOSITORY="Comfy-Org/ComfyUI_frontend"
|
||||
fi
|
||||
|
||||
# Check PR exists and is open
|
||||
PR_STATE=$(gh pr view $PR_NUMBER --repo $REPOSITORY --json state -q .state 2>/dev/null || echo "NOT_FOUND")
|
||||
if [ "$PR_STATE" = "NOT_FOUND" ]; then
|
||||
echo "Error: PR #$PR_NUMBER not found in $REPOSITORY"
|
||||
exit 1
|
||||
elif [ "$PR_STATE" != "OPEN" ]; then
|
||||
echo "Error: PR #$PR_NUMBER is not open (state: $PR_STATE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check API rate limits
|
||||
RATE_REMAINING=$(gh api /rate_limit --jq '.rate.remaining' 2>/dev/null || echo "5000")
|
||||
if [ "$RATE_REMAINING" -lt 100 ]; then
|
||||
echo "Warning: Low API rate limit: $RATE_REMAINING remaining"
|
||||
if [ "$RATE_REMAINING" -lt 50 ]; then
|
||||
echo "Error: Insufficient API rate limit for comprehensive review"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Pre-flight checks passed"
|
||||
}
|
||||
|
||||
# Run pre-flight checks
|
||||
check_prerequisites
|
||||
|
||||
echo "Starting comprehensive review of PR #$PR_NUMBER"
|
||||
|
||||
# Fetch PR information with error handling
|
||||
echo "Fetching PR information..."
|
||||
if ! gh pr view $PR_NUMBER --repo $REPOSITORY --json files,title,body,additions,deletions,baseRefName,headRefName > pr_info.json; then
|
||||
echo "Error: Failed to fetch PR information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract branch names
|
||||
BASE_BRANCH=$(jq -r '.baseRefName' < pr_info.json)
|
||||
HEAD_BRANCH=$(jq -r '.headRefName' < pr_info.json)
|
||||
|
||||
# Checkout PR branch locally for better file inspection
|
||||
echo "Checking out PR branch..."
|
||||
git fetch origin "pull/$PR_NUMBER/head:pr-$PR_NUMBER"
|
||||
git checkout "pr-$PR_NUMBER"
|
||||
|
||||
# Get changed files using git locally (much faster)
|
||||
git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt
|
||||
|
||||
# Get the diff using git locally
|
||||
git diff "origin/$BASE_BRANCH" > pr_diff.txt
|
||||
|
||||
# Get detailed file changes with line numbers
|
||||
git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt
|
||||
|
||||
# For API compatibility, create a simplified pr_files.json
|
||||
echo '[]' > pr_files.json
|
||||
while IFS=$'\t' read -r status file; do
|
||||
if [[ "$status" != "D" ]]; then # Skip deleted files
|
||||
# Get the patch for this file
|
||||
patch=$(git diff "origin/$BASE_BRANCH" -- "$file" | jq -Rs .)
|
||||
additions=$(git diff --numstat "origin/$BASE_BRANCH" -- "$file" | awk '{print $1}')
|
||||
deletions=$(git diff --numstat "origin/$BASE_BRANCH" -- "$file" | awk '{print $2}')
|
||||
|
||||
jq --arg file "$file" \
|
||||
--arg patch "$patch" \
|
||||
--arg additions "$additions" \
|
||||
--arg deletions "$deletions" \
|
||||
'. += [{
|
||||
"filename": $file,
|
||||
"patch": $patch,
|
||||
"additions": ($additions | tonumber),
|
||||
"deletions": ($deletions | tonumber)
|
||||
}]' pr_files.json > pr_files.json.tmp
|
||||
mv pr_files.json.tmp pr_files.json
|
||||
fi
|
||||
done < file_changes.txt
|
||||
|
||||
# Setup caching directory
|
||||
CACHE_DIR=".claude-review-cache"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
# Function to cache analysis results
|
||||
cache_analysis() {
|
||||
local file_path=$1
|
||||
local analysis_result=$2
|
||||
local file_hash=$(git hash-object "$file_path" 2>/dev/null || echo "no-hash")
|
||||
|
||||
if [ "$file_hash" != "no-hash" ]; then
|
||||
echo "$analysis_result" > "$CACHE_DIR/${file_hash}.cache"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get cached analysis
|
||||
get_cached_analysis() {
|
||||
local file_path=$1
|
||||
local file_hash=$(git hash-object "$file_path" 2>/dev/null || echo "no-hash")
|
||||
|
||||
if [ "$file_hash" != "no-hash" ] && [ -f "$CACHE_DIR/${file_hash}.cache" ]; then
|
||||
cat "$CACHE_DIR/${file_hash}.cache"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Clean old cache entries (older than 7 days)
|
||||
find "$CACHE_DIR" -name "*.cache" -mtime +7 -delete 2>/dev/null || true
|
||||
```
|
||||
|
||||
## Phase 2: Load Comprehensive Knowledge Base
|
||||
|
||||
```bash
|
||||
# Don't create knowledge directory until we know we need it
|
||||
KNOWLEDGE_FOUND=false
|
||||
|
||||
# Use local cache for knowledge base to avoid repeated downloads
|
||||
KNOWLEDGE_CACHE_DIR=".claude-knowledge-cache"
|
||||
mkdir -p "$KNOWLEDGE_CACHE_DIR"
|
||||
|
||||
# Option to use cloned prompt library for better performance
|
||||
PROMPT_LIBRARY_PATH="../comfy-claude-prompt-library"
|
||||
if [ -d "$PROMPT_LIBRARY_PATH" ]; then
|
||||
echo "Using local prompt library at $PROMPT_LIBRARY_PATH"
|
||||
USE_LOCAL_PROMPT_LIBRARY=true
|
||||
else
|
||||
echo "No local prompt library found, will use GitHub API"
|
||||
USE_LOCAL_PROMPT_LIBRARY=false
|
||||
fi
|
||||
|
||||
# Function to fetch with cache
|
||||
fetch_with_cache() {
|
||||
local url=$1
|
||||
local output_file=$2
|
||||
local cache_file="$KNOWLEDGE_CACHE_DIR/$(echo "$url" | sed 's/[^a-zA-Z0-9]/_/g')"
|
||||
|
||||
# Check if cached version exists and is less than 1 day old
|
||||
if [ -f "$cache_file" ] && [ $(find "$cache_file" -mtime -1 2>/dev/null | wc -l) -gt 0 ]; then
|
||||
# Create knowledge directory only when we actually have content
|
||||
if [ "$KNOWLEDGE_FOUND" = "false" ]; then
|
||||
mkdir -p review_knowledge
|
||||
KNOWLEDGE_FOUND=true
|
||||
fi
|
||||
cp "$cache_file" "$output_file"
|
||||
echo "Using cached version of $(basename "$output_file")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try to fetch fresh version
|
||||
if curl -s -f "$url" > "$output_file.tmp"; then
|
||||
# Create knowledge directory only when we actually have content
|
||||
if [ "$KNOWLEDGE_FOUND" = "false" ]; then
|
||||
mkdir -p review_knowledge
|
||||
KNOWLEDGE_FOUND=true
|
||||
fi
|
||||
mv "$output_file.tmp" "$output_file"
|
||||
cp "$output_file" "$cache_file"
|
||||
echo "Downloaded fresh version of $(basename "$output_file")"
|
||||
return 0
|
||||
else
|
||||
# If fetch failed but we have a cache, use it
|
||||
if [ -f "$cache_file" ]; then
|
||||
if [ "$KNOWLEDGE_FOUND" = "false" ]; then
|
||||
mkdir -p review_knowledge
|
||||
KNOWLEDGE_FOUND=true
|
||||
fi
|
||||
cp "$cache_file" "$output_file"
|
||||
echo "Using stale cache for $(basename "$output_file") (download failed)"
|
||||
return 0
|
||||
fi
|
||||
echo "Failed to load $(basename "$output_file")"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Load REPOSITORY_GUIDE.md for deep architectural understanding
|
||||
echo "Loading ComfyUI Frontend repository guide..."
|
||||
if [ "$USE_LOCAL_PROMPT_LIBRARY" = "true" ] && [ -f "$PROMPT_LIBRARY_PATH/project-summaries-for-agents/ComfyUI_frontend/REPOSITORY_GUIDE.md" ]; then
|
||||
if [ "$KNOWLEDGE_FOUND" = "false" ]; then
|
||||
mkdir -p review_knowledge
|
||||
KNOWLEDGE_FOUND=true
|
||||
fi
|
||||
cp "$PROMPT_LIBRARY_PATH/project-summaries-for-agents/ComfyUI_frontend/REPOSITORY_GUIDE.md" "review_knowledge/repository_guide.md"
|
||||
echo "Loaded repository guide from local prompt library"
|
||||
else
|
||||
fetch_with_cache "https://raw.githubusercontent.com/Comfy-Org/comfy-claude-prompt-library/master/project-summaries-for-agents/ComfyUI_frontend/REPOSITORY_GUIDE.md" "review_knowledge/repository_guide.md"
|
||||
fi
|
||||
|
||||
# 3. Discover and load relevant knowledge folders from GitHub API
|
||||
echo "Discovering available knowledge folders..."
|
||||
KNOWLEDGE_API_URL="https://api.github.com/repos/Comfy-Org/comfy-claude-prompt-library/contents/.claude/knowledge"
|
||||
if KNOWLEDGE_FOLDERS=$(curl -s "$KNOWLEDGE_API_URL" | jq -r '.[] | select(.type=="dir") | .name' 2>/dev/null); then
|
||||
echo "Available knowledge folders: $KNOWLEDGE_FOLDERS"
|
||||
|
||||
# Analyze changed files to determine which knowledge folders might be relevant
|
||||
CHANGED_FILES=$(cat changed_files.txt)
|
||||
PR_TITLE=$(jq -r '.title' < pr_info.json)
|
||||
PR_BODY=$(jq -r '.body // ""' < pr_info.json)
|
||||
|
||||
# For each knowledge folder, check if it might be relevant to the PR
|
||||
for folder in $KNOWLEDGE_FOLDERS; do
|
||||
# Simple heuristic: if folder name appears in changed file paths or PR context
|
||||
if echo "$CHANGED_FILES $PR_TITLE $PR_BODY" | grep -qi "$folder"; then
|
||||
echo "Loading knowledge folder: $folder"
|
||||
# Fetch all files in that knowledge folder
|
||||
FOLDER_API_URL="https://api.github.com/repos/Comfy-Org/comfy-claude-prompt-library/contents/.claude/knowledge/$folder"
|
||||
curl -s "$FOLDER_API_URL" | jq -r '.[] | select(.type=="file") | .download_url' 2>/dev/null | \
|
||||
while read url; do
|
||||
if [ -n "$url" ]; then
|
||||
filename=$(basename "$url")
|
||||
fetch_with_cache "$url" "review_knowledge/${folder}_${filename}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "Could not discover knowledge folders"
|
||||
fi
|
||||
|
||||
# 4. Load validation rules from the repository
|
||||
echo "Loading validation rules..."
|
||||
VALIDATION_API_URL="https://api.github.com/repos/Comfy-Org/comfy-claude-prompt-library/contents/.claude/commands/validation"
|
||||
if VALIDATION_FILES=$(curl -s "$VALIDATION_API_URL" | jq -r '.[] | select(.name | contains("frontend") or contains("security") or contains("performance")) | .download_url' 2>/dev/null); then
|
||||
for url in $VALIDATION_FILES; do
|
||||
if [ -n "$url" ]; then
|
||||
filename=$(basename "$url")
|
||||
fetch_with_cache "$url" "review_knowledge/validation_${filename}"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "Could not load validation rules"
|
||||
fi
|
||||
|
||||
# 5. Load local project guidelines
|
||||
if [ -f "CLAUDE.md" ]; then
|
||||
if [ "$KNOWLEDGE_FOUND" = "false" ]; then
|
||||
mkdir -p review_knowledge
|
||||
KNOWLEDGE_FOUND=true
|
||||
fi
|
||||
cp CLAUDE.md review_knowledge/local_claude.md
|
||||
fi
|
||||
if [ -f ".github/CLAUDE.md" ]; then
|
||||
if [ "$KNOWLEDGE_FOUND" = "false" ]; then
|
||||
mkdir -p review_knowledge
|
||||
KNOWLEDGE_FOUND=true
|
||||
fi
|
||||
cp .github/CLAUDE.md review_knowledge/github_claude.md
|
||||
fi
|
||||
```
|
||||
|
||||
## Phase 3: Deep Analysis Instructions
|
||||
|
||||
Perform a comprehensive analysis covering these areas:
|
||||
|
||||
### 3.1 Architectural Analysis
|
||||
Based on the repository guide and project summary, evaluate:
|
||||
- Does this change align with the established architecture patterns?
|
||||
- Are domain boundaries respected?
|
||||
- Is the extension system used appropriately?
|
||||
- Are components properly organized by feature?
|
||||
- Does it follow the established service/composable/store patterns?
|
||||
|
||||
### 3.2 Code Quality Beyond Linting
|
||||
- Cyclomatic complexity and cognitive load
|
||||
- SOLID principles adherence
|
||||
- DRY violations that aren't caught by simple duplication checks
|
||||
- Proper abstraction levels
|
||||
- Interface design and API clarity
|
||||
- No leftover debug code (console.log, commented code, TODO comments)
|
||||
|
||||
### 3.3 Library Usage Enforcement
|
||||
CRITICAL: Never re-implement functionality that exists in our standard libraries:
|
||||
- **Tailwind CSS**: Use utility classes instead of custom CSS or style attributes
|
||||
- **PrimeVue**: Never re-implement components that exist in PrimeVue (buttons, modals, dropdowns, etc.)
|
||||
- **VueUse**: Never re-implement composables that exist in VueUse (useLocalStorage, useDebounceFn, etc.)
|
||||
- **Lodash**: Never re-implement utility functions (debounce, throttle, cloneDeep, etc.)
|
||||
- **Common components**: Reuse components from src/components/common/
|
||||
- **DOMPurify**: Always use for HTML sanitization
|
||||
- **Fuse.js**: Use for fuzzy search functionality
|
||||
- **Marked**: Use for markdown parsing
|
||||
- **Pinia**: Use for global state management, not custom solutions
|
||||
- **Zod**: Use for form validation with zodResolver pattern
|
||||
- **Tiptap**: Use for rich text/markdown editing
|
||||
- **Xterm.js**: Use for terminal emulation
|
||||
- **Axios**: Use for HTTP client initialization
|
||||
|
||||
### 3.4 Security Deep Dive
|
||||
Beyond obvious vulnerabilities:
|
||||
- Authentication/authorization implications
|
||||
- Data validation completeness
|
||||
- State management security
|
||||
- Cross-origin concerns
|
||||
- Extension security boundaries
|
||||
|
||||
### 3.5 Performance Analysis
|
||||
- Render performance implications
|
||||
- Layout thrashing prevention
|
||||
- Memory leak potential
|
||||
- Network request optimization
|
||||
- State management efficiency
|
||||
|
||||
### 3.6 Integration Concerns
|
||||
- Breaking changes to internal APIs
|
||||
- Extension compatibility
|
||||
- Backward compatibility
|
||||
- Migration requirements
|
||||
|
||||
## Phase 4: Create Detailed Review Comments
|
||||
|
||||
CRITICAL: Keep comments extremely concise and effective. Use only as many words as absolutely necessary.
|
||||
- NO markdown formatting (no #, ##, ###, **, etc.)
|
||||
- NO emojis
|
||||
- Get to the point immediately
|
||||
- Burden the reader as little as possible
|
||||
|
||||
For each issue found, create a concise inline comment with:
|
||||
1. What's wrong (one line)
|
||||
2. Why it matters (one line)
|
||||
3. How to fix it (one line)
|
||||
4. Code example only if essential
|
||||
|
||||
```bash
|
||||
# Helper function for comprehensive comments
|
||||
post_review_comment() {
|
||||
local file_path=$1
|
||||
local line_number=$2
|
||||
local severity=$3 # critical/high/medium/low
|
||||
local category=$4 # architecture/security/performance/quality
|
||||
local issue=$5
|
||||
local context=$6
|
||||
local suggestion=$7
|
||||
local example=$8
|
||||
|
||||
local body="### [$category] $severity Priority
|
||||
|
||||
**Issue**: $issue
|
||||
|
||||
**Context**: $context
|
||||
|
||||
**Suggestion**: $suggestion"
|
||||
|
||||
if [ -n "$example" ]; then
|
||||
body="$body
|
||||
|
||||
**Example**:
|
||||
\`\`\`typescript
|
||||
$example
|
||||
\`\`\`"
|
||||
fi
|
||||
|
||||
body="$body
|
||||
|
||||
*Related: See [repository guide](https://github.com/Comfy-Org/comfy-claude-prompt-library/blob/master/project-summaries-for-agents/ComfyUI_frontend/REPOSITORY_GUIDE.md) for patterns*"
|
||||
|
||||
gh api -X POST /repos/$REPOSITORY/pulls/$PR_NUMBER/comments \
|
||||
-f path="$file_path" \
|
||||
-f line=$line_number \
|
||||
-f body="$body" \
|
||||
-f commit_id="$COMMIT_SHA" \
|
||||
-f side='RIGHT' || echo "Failed to post comment at $file_path:$line_number"
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 5: Validation Rules Application
|
||||
|
||||
Apply ALL validation rules from the loaded knowledge, but focus on the changed lines:
|
||||
|
||||
### From Frontend Standards
|
||||
- Vue 3 Composition API patterns
|
||||
- Component communication patterns
|
||||
- Proper use of composables
|
||||
- TypeScript strict mode compliance
|
||||
- Bundle optimization
|
||||
|
||||
### From Security Audit
|
||||
- Input validation
|
||||
- XSS prevention
|
||||
- CSRF protection
|
||||
- Secure state management
|
||||
- API security
|
||||
|
||||
### From Performance Check
|
||||
- Render optimization
|
||||
- Memory management
|
||||
- Network efficiency
|
||||
- Bundle size impact
|
||||
|
||||
## Phase 6: Contextual Review Based on PR Type
|
||||
|
||||
Analyze the PR description and changes to determine the type:
|
||||
|
||||
```bash
|
||||
# Extract PR metadata with error handling
|
||||
if [ ! -f pr_info.json ]; then
|
||||
echo "Error: pr_info.json not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_TITLE=$(jq -r '.title // "Unknown"' < pr_info.json)
|
||||
PR_BODY=$(jq -r '.body // ""' < pr_info.json)
|
||||
FILE_COUNT=$(wc -l < changed_files.txt)
|
||||
ADDITIONS=$(jq -r '.additions // 0' < pr_info.json)
|
||||
DELETIONS=$(jq -r '.deletions // 0' < pr_info.json)
|
||||
|
||||
# Determine PR type and apply specific review criteria
|
||||
if echo "$PR_TITLE $PR_BODY" | grep -qiE "(feature|feat)"; then
|
||||
echo "Detected feature PR - applying feature review criteria"
|
||||
# Check for tests, documentation, backward compatibility
|
||||
elif echo "$PR_TITLE $PR_BODY" | grep -qiE "(fix|bug)"; then
|
||||
echo "Detected bug fix - checking root cause and regression tests"
|
||||
# Verify fix addresses root cause, includes tests
|
||||
elif echo "$PR_TITLE $PR_BODY" | grep -qiE "(refactor)"; then
|
||||
echo "Detected refactoring - ensuring behavior preservation"
|
||||
# Check that tests still pass, no behavior changes
|
||||
fi
|
||||
```
|
||||
|
||||
## Phase 7: Generate Comprehensive Summary
|
||||
|
||||
After all inline comments, create a detailed summary:
|
||||
|
||||
```bash
|
||||
# Initialize metrics tracking
|
||||
REVIEW_START_TIME=$(date +%s)
|
||||
|
||||
# Create the comprehensive summary
|
||||
gh pr review $PR_NUMBER --comment --body "# Comprehensive PR Review
|
||||
|
||||
This review is generated by Claude. It may not always be accurate, as with human reviewers. If you believe that any of the comments are invalid or incorrect, please state why for each. For others, please implement the changes in one way or another.
|
||||
|
||||
## Review Summary
|
||||
|
||||
**PR**: $PR_TITLE (#$PR_NUMBER)
|
||||
**Impact**: $ADDITIONS additions, $DELETIONS deletions across $FILE_COUNT files
|
||||
|
||||
### Issue Distribution
|
||||
- Critical: $CRITICAL_COUNT
|
||||
- High: $HIGH_COUNT
|
||||
- Medium: $MEDIUM_COUNT
|
||||
- Low: $LOW_COUNT
|
||||
|
||||
### Category Breakdown
|
||||
- Architecture: $ARCHITECTURE_ISSUES issues
|
||||
- Security: $SECURITY_ISSUES issues
|
||||
- Performance: $PERFORMANCE_ISSUES issues
|
||||
- Code Quality: $QUALITY_ISSUES issues
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Architecture & Design
|
||||
[Detailed architectural analysis based on repository patterns]
|
||||
|
||||
### Security Considerations
|
||||
[Security implications beyond basic vulnerabilities]
|
||||
|
||||
### Performance Impact
|
||||
[Performance analysis including bundle size, render impact]
|
||||
|
||||
### Integration Points
|
||||
[How this affects other systems, extensions, etc.]
|
||||
|
||||
## Positive Observations
|
||||
[What was done well, good patterns followed]
|
||||
|
||||
## References
|
||||
- [Repository Architecture Guide](https://github.com/Comfy-Org/comfy-claude-prompt-library/blob/master/project-summaries-for-agents/ComfyUI_frontend/REPOSITORY_GUIDE.md)
|
||||
- [Frontend Standards](https://github.com/Comfy-Org/comfy-claude-prompt-library/blob/master/.claude/commands/validation/frontend-code-standards.md)
|
||||
- [Security Guidelines](https://github.com/Comfy-Org/comfy-claude-prompt-library/blob/master/.claude/commands/validation/security-audit.md)
|
||||
|
||||
## Next Steps
|
||||
1. Address critical issues before merge
|
||||
2. Consider architectural feedback for long-term maintainability
|
||||
3. Add tests for uncovered scenarios
|
||||
4. Update documentation if needed
|
||||
|
||||
---
|
||||
*This is a comprehensive automated review. For architectural decisions requiring human judgment, please request additional manual review.*"
|
||||
```
|
||||
|
||||
## Important: Think Deeply
|
||||
|
||||
When reviewing:
|
||||
1. **Think hard** about architectural implications
|
||||
2. Consider how changes affect the entire system
|
||||
3. Look for subtle bugs and edge cases
|
||||
4. Evaluate maintainability over time
|
||||
5. Consider extension developer experience
|
||||
6. Think about migration paths
|
||||
|
||||
This is a COMPREHENSIVE review, not a linting pass. Provide the same quality feedback a senior engineer would give after careful consideration.
|
||||
|
||||
## Phase 8: Track Review Metrics
|
||||
|
||||
After completing the review, save metrics for analysis:
|
||||
|
||||
```bash
|
||||
# Calculate review duration
|
||||
REVIEW_END_TIME=$(date +%s)
|
||||
REVIEW_DURATION=$((REVIEW_END_TIME - REVIEW_START_TIME))
|
||||
|
||||
# Calculate total issues
|
||||
TOTAL_ISSUES=$((CRITICAL_COUNT + HIGH_COUNT + MEDIUM_COUNT + LOW_COUNT))
|
||||
|
||||
# Create metrics directory if it doesn't exist
|
||||
METRICS_DIR=".claude/review-metrics"
|
||||
mkdir -p "$METRICS_DIR"
|
||||
|
||||
# Generate metrics file
|
||||
METRICS_FILE="$METRICS_DIR/metrics-$(date +%Y%m).json"
|
||||
|
||||
# Create or update monthly metrics file
|
||||
if [ -f "$METRICS_FILE" ]; then
|
||||
# Append to existing file
|
||||
jq -n \
|
||||
--arg pr "$PR_NUMBER" \
|
||||
--arg title "$PR_TITLE" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg duration "$REVIEW_DURATION" \
|
||||
--arg files "$FILE_COUNT" \
|
||||
--arg additions "$ADDITIONS" \
|
||||
--arg deletions "$DELETIONS" \
|
||||
--arg total "$TOTAL_ISSUES" \
|
||||
--arg critical "$CRITICAL_COUNT" \
|
||||
--arg high "$HIGH_COUNT" \
|
||||
--arg medium "$MEDIUM_COUNT" \
|
||||
--arg low "$LOW_COUNT" \
|
||||
--arg architecture "$ARCHITECTURE_ISSUES" \
|
||||
--arg security "$SECURITY_ISSUES" \
|
||||
--arg performance "$PERFORMANCE_ISSUES" \
|
||||
--arg quality "$QUALITY_ISSUES" \
|
||||
'{
|
||||
pr_number: $pr,
|
||||
pr_title: $title,
|
||||
timestamp: $timestamp,
|
||||
review_duration_seconds: ($duration | tonumber),
|
||||
files_reviewed: ($files | tonumber),
|
||||
lines_added: ($additions | tonumber),
|
||||
lines_deleted: ($deletions | tonumber),
|
||||
issues: {
|
||||
total: ($total | tonumber),
|
||||
by_severity: {
|
||||
critical: ($critical | tonumber),
|
||||
high: ($high | tonumber),
|
||||
medium: ($medium | tonumber),
|
||||
low: ($low | tonumber)
|
||||
},
|
||||
by_category: {
|
||||
architecture: ($architecture | tonumber),
|
||||
security: ($security | tonumber),
|
||||
performance: ($performance | tonumber),
|
||||
quality: ($quality | tonumber)
|
||||
}
|
||||
}
|
||||
}' > "$METRICS_FILE.new"
|
||||
|
||||
# Merge with existing data
|
||||
jq -s '.[0] + [.[1]]' "$METRICS_FILE" "$METRICS_FILE.new" > "$METRICS_FILE.tmp"
|
||||
mv "$METRICS_FILE.tmp" "$METRICS_FILE"
|
||||
rm "$METRICS_FILE.new"
|
||||
else
|
||||
# Create new file
|
||||
jq -n \
|
||||
--arg pr "$PR_NUMBER" \
|
||||
--arg title "$PR_TITLE" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg duration "$REVIEW_DURATION" \
|
||||
--arg files "$FILE_COUNT" \
|
||||
--arg additions "$ADDITIONS" \
|
||||
--arg deletions "$DELETIONS" \
|
||||
--arg total "$TOTAL_ISSUES" \
|
||||
--arg critical "$CRITICAL_COUNT" \
|
||||
--arg high "$HIGH_COUNT" \
|
||||
--arg medium "$MEDIUM_COUNT" \
|
||||
--arg low "$LOW_COUNT" \
|
||||
--arg architecture "$ARCHITECTURE_ISSUES" \
|
||||
--arg security "$SECURITY_ISSUES" \
|
||||
--arg performance "$PERFORMANCE_ISSUES" \
|
||||
--arg quality "$QUALITY_ISSUES" \
|
||||
'[{
|
||||
pr_number: $pr,
|
||||
pr_title: $title,
|
||||
timestamp: $timestamp,
|
||||
review_duration_seconds: ($duration | tonumber),
|
||||
files_reviewed: ($files | tonumber),
|
||||
lines_added: ($additions | tonumber),
|
||||
lines_deleted: ($deletions | tonumber),
|
||||
issues: {
|
||||
total: ($total | tonumber),
|
||||
by_severity: {
|
||||
critical: ($critical | tonumber),
|
||||
high: ($high | tonumber),
|
||||
medium: ($medium | tonumber),
|
||||
low: ($low | tonumber)
|
||||
},
|
||||
by_category: {
|
||||
architecture: ($architecture | tonumber),
|
||||
security: ($security | tonumber),
|
||||
performance: ($performance | tonumber),
|
||||
quality: ($quality | tonumber)
|
||||
}
|
||||
}
|
||||
}]' > "$METRICS_FILE"
|
||||
fi
|
||||
|
||||
echo "Review metrics saved to $METRICS_FILE"
|
||||
```
|
||||
|
||||
This creates monthly metrics files (e.g., `metrics-202407.json`) that track:
|
||||
- Which PRs were reviewed
|
||||
- How long reviews took
|
||||
- Types and severity of issues found
|
||||
- Trends over time
|
||||
|
||||
You can later analyze these to see patterns and improve your development process.
|
||||
625
.claude/commands/create-frontend-release.md
Normal file
@@ -0,0 +1,625 @@
|
||||
# Create Frontend Release
|
||||
|
||||
This command guides you through creating a comprehensive frontend release with semantic versioning analysis, automated change detection, security scanning, and multi-stage human verification.
|
||||
|
||||
<task>
|
||||
Create a frontend release with version type: $ARGUMENTS
|
||||
|
||||
Expected format: Version increment type and optional description
|
||||
Examples:
|
||||
- `patch` - Bug fixes only
|
||||
- `minor` - New features, backward compatible
|
||||
- `major` - Breaking changes
|
||||
- `prerelease` - Alpha/beta/rc releases
|
||||
- `patch "Critical security fixes"` - With custom description
|
||||
- `minor --skip-changelog` - Skip automated changelog generation
|
||||
- `minor --dry-run` - Simulate release without executing
|
||||
|
||||
If no arguments provided, the command will always perform prerelease if the current version is prerelease, or patch in other cases. This command will never perform minor or major releases without explicit direction.
|
||||
</task>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure:
|
||||
- You have push access to the repository
|
||||
- GitHub CLI (`gh`) is authenticated
|
||||
- You're on a clean main branch working tree
|
||||
- All intended changes are merged to main
|
||||
- You understand the scope of changes being released
|
||||
|
||||
## Critical Checks Before Starting
|
||||
|
||||
### 1. Check Current Version Status
|
||||
```bash
|
||||
# Get current version and check if it's a pre-release
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
if [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
|
||||
echo "⚠️ Current version $CURRENT_VERSION is a pre-release"
|
||||
echo "Consider releasing stable (e.g., 1.24.0-1 → 1.24.0) first"
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. Find Last Stable Release
|
||||
```bash
|
||||
# Get last stable release tag (no pre-release suffix)
|
||||
LAST_STABLE=$(git tag -l "v*" | grep -v "\-" | sort -V | tail -1)
|
||||
echo "Last stable release: $LAST_STABLE"
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
**Environment Variables:**
|
||||
- `RELEASE_SKIP_SECURITY_SCAN=true` - Skip security audit
|
||||
- `RELEASE_AUTO_APPROVE=true` - Skip some confirmation prompts
|
||||
- `RELEASE_DRY_RUN=true` - Simulate release without executing
|
||||
|
||||
## Release Process
|
||||
|
||||
### Step 1: Environment Safety Check
|
||||
|
||||
1. Verify clean working directory:
|
||||
```bash
|
||||
git status --porcelain
|
||||
```
|
||||
2. Confirm on main branch:
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
3. Pull latest changes:
|
||||
```bash
|
||||
git pull origin main
|
||||
```
|
||||
4. Check GitHub CLI authentication:
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
5. Verify npm/PyPI publishing access (dry run)
|
||||
6. **CONFIRMATION REQUIRED**: Environment ready for release?
|
||||
|
||||
### Step 2: Analyze Recent Changes
|
||||
|
||||
1. Get current version from package.json
|
||||
2. **IMPORTANT**: Determine correct base for comparison:
|
||||
```bash
|
||||
# If current version is pre-release, use last stable release
|
||||
if [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
|
||||
BASE_TAG=$LAST_STABLE
|
||||
else
|
||||
BASE_TAG=$(git describe --tags --abbrev=0)
|
||||
fi
|
||||
```
|
||||
3. Find commits since base release (CRITICAL: use --first-parent):
|
||||
```bash
|
||||
git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent
|
||||
```
|
||||
4. Count total commits:
|
||||
```bash
|
||||
COMMIT_COUNT=$(git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent | wc -l)
|
||||
echo "Found $COMMIT_COUNT commits since $BASE_TAG"
|
||||
```
|
||||
5. Analyze commits for:
|
||||
- Breaking changes (BREAKING CHANGE, !, feat())
|
||||
- New features (feat:, feature:)
|
||||
- Bug fixes (fix:, bugfix:)
|
||||
- Documentation changes (docs:)
|
||||
- Dependency updates
|
||||
6. **VERIFY PR TARGET BRANCHES**:
|
||||
```bash
|
||||
# Get merged PRs and verify they were merged to main
|
||||
gh pr list --state merged --limit 50 --json number,title,baseRefName,mergedAt | \
|
||||
jq -r '.[] | select(.baseRefName == "main") | "\(.number): \(.title)"'
|
||||
```
|
||||
7. **HUMAN ANALYSIS**: Review change summary and verify scope
|
||||
|
||||
### Step 3: Version Preview
|
||||
|
||||
**Version Preview:**
|
||||
- Current: `${CURRENT_VERSION}`
|
||||
- Proposed: Show exact version number
|
||||
- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
|
||||
|
||||
### Step 4: Security and Dependency Audit
|
||||
|
||||
1. Run security audit:
|
||||
```bash
|
||||
npm audit --audit-level moderate
|
||||
```
|
||||
2. Check for known vulnerabilities in dependencies
|
||||
3. Scan for hardcoded secrets or credentials:
|
||||
```bash
|
||||
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
|
||||
```
|
||||
4. Verify no sensitive data in recent commits
|
||||
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
|
||||
|
||||
### Step 5: Pre-Release Testing
|
||||
|
||||
1. Run complete test suite:
|
||||
```bash
|
||||
npm run test:unit
|
||||
npm run test:component
|
||||
npm run test:browser
|
||||
```
|
||||
2. Run type checking:
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
3. Run linting (may have issues with missing packages):
|
||||
```bash
|
||||
npm run lint || echo "Lint issues - verify if critical"
|
||||
```
|
||||
4. Test build process:
|
||||
```bash
|
||||
npm run build
|
||||
npm run build:types
|
||||
```
|
||||
5. **QUALITY GATE**: All tests and builds passing?
|
||||
|
||||
### Step 6: Breaking Change Analysis
|
||||
|
||||
1. Analyze API changes in:
|
||||
- Public TypeScript interfaces
|
||||
- Extension APIs
|
||||
- Component props
|
||||
- CLAUDE.md guidelines
|
||||
2. Check for:
|
||||
- Removed public functions/classes
|
||||
- Changed function signatures
|
||||
- Deprecated feature removals
|
||||
- Configuration changes
|
||||
3. Generate breaking change summary
|
||||
4. **COMPATIBILITY REVIEW**: Breaking changes documented and justified?
|
||||
|
||||
### Step 7: Generate and Save Changelog
|
||||
|
||||
1. Extract commit messages since base release:
|
||||
```bash
|
||||
git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent > commits.txt
|
||||
```
|
||||
2. **CRITICAL**: Verify PR inclusion by checking merge location:
|
||||
```bash
|
||||
# For each significant PR mentioned, verify it's on main
|
||||
for PR in ${SIGNIFICANT_PRS}; do
|
||||
COMMIT=$(gh pr view $PR --json mergeCommit -q .mergeCommit.oid)
|
||||
git branch -r --contains $COMMIT | grep -q "origin/main" || \
|
||||
echo "WARNING: PR #$PR not on main branch!"
|
||||
done
|
||||
```
|
||||
3. Group by type:
|
||||
- 🚀 **Features** (feat:)
|
||||
- 🐛 **Bug Fixes** (fix:)
|
||||
- 💥 **Breaking Changes** (BREAKING CHANGE)
|
||||
- 📚 **Documentation** (docs:)
|
||||
- 🔧 **Maintenance** (chore:, refactor:)
|
||||
- ⬆️ **Dependencies** (deps:, dependency updates)
|
||||
4. Include PR numbers and links
|
||||
5. Add issue references (Fixes #123)
|
||||
6. **Save changelog locally:**
|
||||
```bash
|
||||
# Save to dated file for history
|
||||
echo "$CHANGELOG" > release-notes-${NEW_VERSION}-$(date +%Y%m%d).md
|
||||
|
||||
# Save to current for easy access
|
||||
echo "$CHANGELOG" > CURRENT_RELEASE_NOTES.md
|
||||
```
|
||||
7. **CHANGELOG REVIEW**: Verify all PRs listed are actually on main branch
|
||||
|
||||
### Step 8: Create Enhanced Release Notes
|
||||
|
||||
1. Create comprehensive user-facing release notes including:
|
||||
- **What's New**: Major features and improvements
|
||||
- **Bug Fixes**: User-visible fixes
|
||||
- **Breaking Changes**: Migration guide if applicable
|
||||
- **Dependencies**: Major dependency updates
|
||||
- **Performance**: Notable performance improvements
|
||||
- **Contributors**: Thank contributors for their work
|
||||
2. Reference related documentation updates
|
||||
3. Include screenshots for UI changes (if available)
|
||||
4. **Save release notes:**
|
||||
```bash
|
||||
# Enhanced release notes for GitHub
|
||||
echo "$RELEASE_NOTES" > github-release-notes-${NEW_VERSION}.md
|
||||
```
|
||||
5. **CONTENT REVIEW**: Release notes clear and helpful for users?
|
||||
|
||||
### Step 9: Create Version Bump PR
|
||||
|
||||
**For standard version bumps (patch/minor/major):**
|
||||
```bash
|
||||
# Trigger the workflow
|
||||
gh workflow run version-bump.yaml -f version_type=${VERSION_TYPE}
|
||||
|
||||
# Workflow runs quickly - usually creates PR within 30 seconds
|
||||
echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
|
||||
**For releasing a stable version:**
|
||||
1. Must manually create branch and update version:
|
||||
```bash
|
||||
git checkout -b version-bump-${NEW_VERSION}
|
||||
# Edit package.json to remove pre-release suffix
|
||||
git add package.json
|
||||
git commit -m "${NEW_VERSION}"
|
||||
git push origin version-bump-${NEW_VERSION}
|
||||
```
|
||||
|
||||
2. Wait for PR creation (if using workflow) or create manually:
|
||||
```bash
|
||||
# For workflow-created PRs - wait and find it
|
||||
sleep 30
|
||||
# Look for PR from comfy-pr-bot (not github-actions)
|
||||
PR_NUMBER=$(gh pr list --author comfy-pr-bot --limit 1 --json number --jq '.[0].number')
|
||||
|
||||
# Verify we got the PR
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "PR not found yet. Checking recent PRs..."
|
||||
gh pr list --limit 5 --json number,title,author
|
||||
fi
|
||||
|
||||
# For manual PRs
|
||||
gh pr create --title "${NEW_VERSION}" \
|
||||
--body-file enhanced-pr-description.md \
|
||||
--label "Release"
|
||||
```
|
||||
3. **Create enhanced PR description:**
|
||||
```bash
|
||||
cat > enhanced-pr-description.md << EOF
|
||||
# Release v${NEW_VERSION}
|
||||
|
||||
## Version Change
|
||||
\`${CURRENT_VERSION}\` → \`${NEW_VERSION}\` (${VERSION_TYPE})
|
||||
|
||||
## Changelog
|
||||
${CHANGELOG}
|
||||
|
||||
## Breaking Changes
|
||||
${BREAKING_CHANGES}
|
||||
|
||||
## Testing Performed
|
||||
- ✅ Full test suite (unit, component, browser)
|
||||
- ✅ TypeScript compilation
|
||||
- ✅ Linting checks
|
||||
- ✅ Build verification
|
||||
- ✅ Security audit
|
||||
|
||||
## Distribution Channels
|
||||
- GitHub Release (with dist.zip)
|
||||
- PyPI Package (comfyui-frontend-package)
|
||||
- npm Package (@comfyorg/comfyui-frontend-types)
|
||||
|
||||
## Post-Release Tasks
|
||||
- [ ] Verify all distribution channels
|
||||
- [ ] Update external documentation
|
||||
- [ ] Monitor for issues
|
||||
EOF
|
||||
```
|
||||
4. Update PR with enhanced description:
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --body-file enhanced-pr-description.md
|
||||
```
|
||||
5. Add changelog as comment for easy reference:
|
||||
```bash
|
||||
gh pr comment ${PR_NUMBER} --body-file CURRENT_RELEASE_NOTES.md
|
||||
```
|
||||
6. **PR REVIEW**: Version bump PR created and enhanced correctly?
|
||||
|
||||
### Step 11: Critical Release PR Verification
|
||||
|
||||
1. **CRITICAL**: Verify PR has "Release" label:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json labels | jq -r '.labels[].name' | grep -q "Release" || \
|
||||
echo "ERROR: Release label missing! Add it immediately!"
|
||||
```
|
||||
2. Check for update-locales commits:
|
||||
```bash
|
||||
# WARNING: update-locales may add [skip ci] which blocks release workflow!
|
||||
gh pr view ${PR_NUMBER} --json commits | grep -q "skip ci" && \
|
||||
echo "WARNING: [skip ci] detected - release workflow may not trigger!"
|
||||
```
|
||||
3. Verify version number in package.json
|
||||
4. Review all changed files
|
||||
5. Ensure no unintended changes included
|
||||
6. Wait for required PR checks:
|
||||
```bash
|
||||
gh pr checks ${PR_NUMBER} --watch
|
||||
```
|
||||
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
|
||||
|
||||
### Step 12: Pre-Merge Validation
|
||||
|
||||
1. **Review Requirements**: Release PRs require approval
|
||||
2. Monitor CI checks - watch for update-locales
|
||||
3. **CRITICAL WARNING**: If update-locales adds [skip ci], the release workflow won't trigger!
|
||||
4. Check no new commits to main since PR creation
|
||||
5. **DEPLOYMENT READINESS**: Ready to merge?
|
||||
|
||||
### Step 13: Execute Release
|
||||
|
||||
1. **FINAL CONFIRMATION**: Merge PR to trigger release?
|
||||
2. Merge the Release PR:
|
||||
```bash
|
||||
gh pr merge ${PR_NUMBER} --merge
|
||||
```
|
||||
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
|
||||
```bash
|
||||
sleep 10
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
```
|
||||
4. If workflow didn't trigger due to [skip ci]:
|
||||
```bash
|
||||
echo "ERROR: Release workflow didn't trigger!"
|
||||
echo "Options:"
|
||||
echo "1. Create patch release (e.g., 1.24.1) to trigger workflow"
|
||||
echo "2. Investigate manual release options"
|
||||
```
|
||||
5. If workflow triggered, monitor execution:
|
||||
```bash
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
gh run watch ${WORKFLOW_RUN_ID}
|
||||
```
|
||||
|
||||
### Step 14: Enhance GitHub Release
|
||||
|
||||
1. Wait for automatic release creation:
|
||||
```bash
|
||||
# Wait for release to be created
|
||||
while ! gh release view v${NEW_VERSION} >/dev/null 2>&1; do
|
||||
echo "Waiting for release creation..."
|
||||
sleep 10
|
||||
done
|
||||
```
|
||||
|
||||
2. **Enhance the GitHub release:**
|
||||
```bash
|
||||
# Update release with our enhanced notes
|
||||
gh release edit v${NEW_VERSION} \
|
||||
--title "🚀 ComfyUI Frontend v${NEW_VERSION}" \
|
||||
--notes-file github-release-notes-${NEW_VERSION}.md \
|
||||
--latest
|
||||
|
||||
# Add any additional assets if needed
|
||||
# gh release upload v${NEW_VERSION} additional-assets.zip
|
||||
```
|
||||
|
||||
3. **Verify release details:**
|
||||
```bash
|
||||
gh release view v${NEW_VERSION}
|
||||
```
|
||||
|
||||
### Step 15: Verify Multi-Channel Distribution
|
||||
|
||||
1. **GitHub Release:**
|
||||
```bash
|
||||
gh release view v${NEW_VERSION} --json assets,body,createdAt,tagName
|
||||
```
|
||||
- ✅ Check release notes
|
||||
- ✅ Verify dist.zip attachment
|
||||
- ✅ Confirm release marked as latest (for main branch)
|
||||
|
||||
2. **PyPI Package:**
|
||||
```bash
|
||||
# Check PyPI availability (may take a few minutes)
|
||||
for i in {1..10}; do
|
||||
if curl -s https://pypi.org/pypi/comfyui-frontend-package/json | jq -r '.releases | keys[]' | grep -q ${NEW_VERSION}; then
|
||||
echo "✅ PyPI package available"
|
||||
break
|
||||
fi
|
||||
echo "⏳ Waiting for PyPI package... (attempt $i/10)"
|
||||
sleep 30
|
||||
done
|
||||
```
|
||||
|
||||
3. **npm Package:**
|
||||
```bash
|
||||
# Check npm availability
|
||||
for i in {1..10}; do
|
||||
if npm view @comfyorg/comfyui-frontend-types@${NEW_VERSION} version >/dev/null 2>&1; then
|
||||
echo "✅ npm package available"
|
||||
break
|
||||
fi
|
||||
echo "⏳ Waiting for npm package... (attempt $i/10)"
|
||||
sleep 30
|
||||
done
|
||||
```
|
||||
|
||||
4. **DISTRIBUTION VERIFICATION**: All channels published successfully?
|
||||
|
||||
### Step 16: Post-Release Monitoring Setup
|
||||
|
||||
1. **Monitor immediate release health:**
|
||||
```bash
|
||||
# Check for immediate issues
|
||||
gh issue list --label "bug" --state open --limit 5 --json title,number,createdAt
|
||||
|
||||
# Monitor download metrics (if accessible)
|
||||
gh release view v${NEW_VERSION} --json assets --jq '.assets[].downloadCount'
|
||||
```
|
||||
|
||||
2. **Update documentation tracking:**
|
||||
```bash
|
||||
cat > post-release-checklist.md << EOF
|
||||
# Post-Release Checklist for v${NEW_VERSION}
|
||||
|
||||
## Immediate Tasks (Next 24 hours)
|
||||
- [ ] Monitor error rates and user feedback
|
||||
- [ ] Watch for critical issues
|
||||
- [ ] Verify documentation is up to date
|
||||
- [ ] Check community channels for questions
|
||||
|
||||
## Short-term Tasks (Next week)
|
||||
- [ ] Update external integration guides
|
||||
- [ ] Monitor adoption metrics
|
||||
- [ ] Gather user feedback
|
||||
- [ ] Plan next release cycle
|
||||
|
||||
## Long-term Tasks
|
||||
- [ ] Analyze release process improvements
|
||||
- [ ] Update release templates based on learnings
|
||||
- [ ] Document any new patterns discovered
|
||||
|
||||
## Key Metrics to Track
|
||||
- Download counts: GitHub, PyPI, npm
|
||||
- Issue reports related to v${NEW_VERSION}
|
||||
- Community feedback and adoption
|
||||
- Performance impact measurements
|
||||
EOF
|
||||
```
|
||||
|
||||
3. **Create release summary:**
|
||||
```bash
|
||||
cat > release-summary-${NEW_VERSION}.md << EOF
|
||||
# Release Summary: ComfyUI Frontend v${NEW_VERSION}
|
||||
|
||||
**Released:** $(date)
|
||||
**Type:** ${VERSION_TYPE}
|
||||
**Duration:** ~${RELEASE_DURATION} minutes
|
||||
**Release Commit:** ${RELEASE_COMMIT}
|
||||
|
||||
## Metrics
|
||||
- **Commits Included:** ${COMMITS_COUNT}
|
||||
- **Contributors:** ${CONTRIBUTORS_COUNT}
|
||||
- **Files Changed:** ${FILES_CHANGED}
|
||||
- **Lines Added/Removed:** +${LINES_ADDED}/-${LINES_REMOVED}
|
||||
|
||||
## Distribution Status
|
||||
- ✅ GitHub Release: Published
|
||||
- ✅ PyPI Package: Available
|
||||
- ✅ npm Types: Available
|
||||
|
||||
## Next Steps
|
||||
- Monitor for 24-48 hours
|
||||
- Address any critical issues immediately
|
||||
- Plan next release cycle
|
||||
|
||||
## Files Generated
|
||||
- \`release-notes-${NEW_VERSION}-$(date +%Y%m%d).md\` - Detailed changelog
|
||||
- \`github-release-notes-${NEW_VERSION}.md\` - GitHub release notes
|
||||
- \`post-release-checklist.md\` - Follow-up tasks
|
||||
EOF
|
||||
```
|
||||
|
||||
4. **RELEASE COMPLETION**: All post-release setup completed?
|
||||
|
||||
## Advanced Safety Features
|
||||
|
||||
### Rollback Procedures
|
||||
|
||||
**Pre-Merge Rollback:**
|
||||
```bash
|
||||
# Close version bump PR and reset
|
||||
gh pr close ${PR_NUMBER}
|
||||
git reset --hard origin/main
|
||||
git clean -fd
|
||||
```
|
||||
|
||||
**Post-Merge Rollback:**
|
||||
```bash
|
||||
# Create immediate patch release with reverts
|
||||
git revert ${RELEASE_COMMIT}
|
||||
# Follow this command again with patch version
|
||||
```
|
||||
|
||||
**Emergency Procedures:**
|
||||
```bash
|
||||
# Document incident
|
||||
cat > release-incident-${NEW_VERSION}.md << EOF
|
||||
# Release Incident Report
|
||||
|
||||
**Version:** ${NEW_VERSION}
|
||||
**Issue:** [Describe the problem]
|
||||
**Impact:** [Severity and scope]
|
||||
**Resolution:** [Steps taken]
|
||||
**Prevention:** [Future improvements]
|
||||
EOF
|
||||
|
||||
# Contact package registries for critical issues
|
||||
echo "For critical security issues, consider:"
|
||||
echo "- PyPI: Contact support for package yanking"
|
||||
echo "- npm: Use 'npm unpublish' within 72 hours"
|
||||
echo "- GitHub: Update release with warning notes"
|
||||
```
|
||||
|
||||
### Quality Gates Summary
|
||||
|
||||
The command implements multiple quality gates:
|
||||
|
||||
1. **🔒 Security Gate**: Vulnerability scanning, secret detection
|
||||
2. **🧪 Quality Gate**: Full test suite, linting, type checking
|
||||
3. **📋 Content Gate**: Changelog accuracy, release notes quality
|
||||
4. **🔄 Process Gate**: Release timing verification
|
||||
5. **✅ Verification Gate**: Multi-channel publishing confirmation
|
||||
6. **📊 Monitoring Gate**: Post-release health tracking
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: Regular Feature Release
|
||||
```bash
|
||||
/project:create-frontend-release minor
|
||||
```
|
||||
- Analyzes features since last release
|
||||
- Generates changelog automatically
|
||||
- Creates comprehensive release notes
|
||||
|
||||
### Scenario 2: Critical Security Patch
|
||||
```bash
|
||||
/project:create-frontend-release patch "Security fixes for CVE-2024-XXXX"
|
||||
```
|
||||
- Expedited security scanning
|
||||
- Enhanced monitoring setup
|
||||
|
||||
### Scenario 3: Major Version with Breaking Changes
|
||||
```bash
|
||||
/project:create-frontend-release major
|
||||
```
|
||||
- Comprehensive breaking change analysis
|
||||
- Migration guide generation
|
||||
|
||||
### Scenario 4: Pre-release Testing
|
||||
```bash
|
||||
/project:create-frontend-release prerelease
|
||||
```
|
||||
- Creates alpha/beta/rc versions
|
||||
- Draft release status
|
||||
- Python package specs require that prereleases use alpha/beta/rc as the preid
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Pre-release Version Confusion
|
||||
**Problem**: Not sure whether to promote pre-release or create new version
|
||||
**Solution**:
|
||||
- Follow semver standards: a prerelease version is followed by a normal release. It should have the same major, minor, and patch versions as the prerelease.
|
||||
|
||||
### Issue: Wrong Commit Count
|
||||
**Problem**: Changelog includes commits from other branches
|
||||
**Solution**: Always use `--first-parent` flag with git log
|
||||
|
||||
**Update**: Sometimes update-locales doesn't add [skip ci] - always verify!
|
||||
|
||||
### Issue: Missing PRs in Changelog
|
||||
**Problem**: PR was merged to different branch
|
||||
**Solution**: Verify PR merge target with:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json baseRefName
|
||||
```
|
||||
|
||||
### Issue: Release Failed Due to [skip ci]
|
||||
**Problem**: Release workflow didn't trigger after merge
|
||||
**Prevention**: Always avoid this scenario
|
||||
- Ensure that `[skip ci]` or similar flags are NOT in the `HEAD` commit message of the PR
|
||||
- Push a new, empty commit to the PR
|
||||
- Always double-check this immediately before merging
|
||||
|
||||
**Recovery Strategy**:
|
||||
1. Revert version in a new PR (e.g., 1.24.0 → 1.24.0-1)
|
||||
2. Merge the revert PR
|
||||
3. Run version bump workflow again
|
||||
4. This creates a fresh PR without [skip ci]
|
||||
Benefits: Cleaner than creating extra version numbers
|
||||
|
||||
## Key Learnings & Notes
|
||||
|
||||
1. **PR Author**: Version bump PRs are created by `comfy-pr-bot`, not `github-actions`
|
||||
2. **Workflow Speed**: Version bump workflow typically completes in ~20-30 seconds
|
||||
3. **Update-locales Behavior**: Inconsistent - sometimes adds [skip ci], sometimes doesn't
|
||||
4. **Recovery Options**: Reverting version is cleaner than creating extra versions
|
||||
|
||||
36
.github/CLAUDE.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# ComfyUI Frontend - Claude Review Context
|
||||
|
||||
This file provides additional context for the automated PR review system.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### PrimeVue Component Migrations
|
||||
|
||||
When reviewing, flag these deprecated components:
|
||||
- `Dropdown` → Use `Select` from 'primevue/select'
|
||||
- `OverlayPanel` → Use `Popover` from 'primevue/popover'
|
||||
- `Calendar` → Use `DatePicker` from 'primevue/datepicker'
|
||||
- `InputSwitch` → Use `ToggleSwitch` from 'primevue/toggleswitch'
|
||||
- `Sidebar` → Use `Drawer` from 'primevue/drawer'
|
||||
- `Chips` → Use `AutoComplete` with multiple enabled and typeahead disabled
|
||||
- `TabMenu` → Use `Tabs` without panels
|
||||
- `Steps` → Use `Stepper` without panels
|
||||
- `InlineMessage` → Use `Message` component
|
||||
|
||||
### API Utilities Reference
|
||||
|
||||
- `api.apiURL()` - Backend API calls (/prompt, /queue, /view, etc.)
|
||||
- `api.fileURL()` - Static file access (templates, extensions)
|
||||
- `$t()` / `i18n.global.t()` - Internationalization
|
||||
- `DOMPurify.sanitize()` - HTML sanitization
|
||||
|
||||
## Review Scope
|
||||
|
||||
This automated review performs comprehensive analysis including:
|
||||
- Architecture and design patterns
|
||||
- Security vulnerabilities
|
||||
- Performance implications
|
||||
- Code quality and maintainability
|
||||
- Integration concerns
|
||||
|
||||
For implementation details, see `.claude/commands/comprehensive-pr-review.md`.
|
||||
75
.github/workflows/claude-pr-review.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Claude PR Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
wait-for-ci:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'claude-review'
|
||||
outputs:
|
||||
should-proceed: ${{ steps.check-status.outputs.proceed }}
|
||||
steps:
|
||||
- name: Wait for other CI checks
|
||||
uses: lewagon/wait-on-check-action@v1.3.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
check-regexp: '^(ESLint|Prettier Check|Tests CI|Vitest Tests)'
|
||||
wait-interval: 30
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if we should proceed
|
||||
id: check-status
|
||||
run: |
|
||||
# Get all check runs for this commit
|
||||
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("ESLint|Prettier Check|Tests CI|Vitest Tests")) | {name, conclusion}')
|
||||
|
||||
# Check if any required checks failed
|
||||
if echo "$CHECK_RUNS" | grep -q '"conclusion": "failure"'; then
|
||||
echo "Some CI checks failed - skipping Claude review"
|
||||
echo "proceed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "All CI checks passed - proceeding with Claude review"
|
||||
echo "proceed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
claude-review:
|
||||
needs: wait-for-ci
|
||||
if: needs.wait-for-ci.outputs.should-proceed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies for analysis tools
|
||||
run: |
|
||||
npm install -g typescript @vue/compiler-sfc
|
||||
|
||||
- name: Run Claude PR Review
|
||||
uses: anthropics/claude-code-action@main
|
||||
with:
|
||||
prompt_file: .claude/commands/comprehensive-pr-review.md
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
max_turns: 1
|
||||
timeout_minutes: 30
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
27
.github/workflows/danger.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Danger PR Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Danger
|
||||
run: npx danger ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
4
.github/workflows/test-ui.yaml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
- name: Start ComfyUI server
|
||||
run: |
|
||||
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
|
||||
python main.py --cpu --multi-user --cache-none --front-end-root ../ComfyUI_frontend/dist &
|
||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||
working-directory: ComfyUI
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
run: npx playwright test --project=${{ matrix.browser }}
|
||||
run: npx playwright test --project=${{ matrix.browser }} --workers=1
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -9,9 +9,10 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
|
||||
`
|
||||
});
|
||||
|
||||
@@ -529,7 +529,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
### Prerequisites & Technology Stack
|
||||
|
||||
- **Required Software**:
|
||||
- Node.js (v16 or later) and npm
|
||||
- Node.js (v16 or later; v20/v22 strongly recommended) and npm
|
||||
- Git for version control
|
||||
- A running ComfyUI backend instance
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` dir
|
||||
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
|
||||
|
||||
### Node.js & Playwright Prerequisites
|
||||
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
|
||||
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
|
||||
```bash
|
||||
npx playwright install chromium --with-deps
|
||||
```
|
||||
@@ -26,6 +26,12 @@ The `.env` file will not exist until you create it yourself.
|
||||
|
||||
A template with helpful information can be found in `.env_example`.
|
||||
|
||||
### Running ComfyUI Backend
|
||||
When running browser tests, ComfyUI must be started with the `--cache-none` argument to disable execution caching:
|
||||
```bash
|
||||
python main.py --cache-none
|
||||
```
|
||||
|
||||
### Multiple Tests
|
||||
If you are running Playwright tests in parallel or running the same test multiple times, the flag `--multi-user` must be added to the main ComfyUI process.
|
||||
|
||||
|
||||
293
browser_tests/assets/execution/nested-subgraph-test.json
Normal file
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"id": "test-subgraph-workflow",
|
||||
"revision": 0,
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 10,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 200],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [1], "slot_index": 0 },
|
||||
{ "name": "MASK", "type": "MASK", "links": null, "slot_index": 1 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "LoadImage" },
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "test-subgraph-1",
|
||||
"pos": [400, 200],
|
||||
"size": [200, 80],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [9] }
|
||||
],
|
||||
"title": "Test Subgraph",
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "SaveImage",
|
||||
"pos": [700, 200],
|
||||
"size": [315, 270],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "images", "type": "IMAGE", "link": 9 }
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 10, 0, "IMAGE"],
|
||||
[9, 10, 0, 11, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "test-subgraph-1",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 7,
|
||||
"lastLinkId": 6,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Test Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [-154, 200, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [800, 200, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "input-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [1],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": -134,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [5],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": 820,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "TestSleep",
|
||||
"pos": [100, 200],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [2], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.0]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "test-subgraph-2",
|
||||
"pos": [350, 200],
|
||||
"size": [200, 80],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "link": 2 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [5] }
|
||||
],
|
||||
"title": "Nested Test Subgraph",
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": 5,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"origin_id": 5,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "test-subgraph-2",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 7,
|
||||
"lastLinkId": 4,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Nested Test Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [-154, 200, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [600, 200, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "input-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [1],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": -134,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "output-1",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"linkIds": [4],
|
||||
"localized_name": "IMAGE",
|
||||
"pos": {
|
||||
"0": 620,
|
||||
"1": 220
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 6,
|
||||
"type": "TestSleep",
|
||||
"pos": [100, 150],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [2], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.0]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "TestAsyncProgressNode",
|
||||
"pos": [350, 150],
|
||||
"size": [210, 126],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 2 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [4], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestAsyncProgressNode" },
|
||||
"widgets_values": [3.0, 10]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 6,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": 7,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "IMAGE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
139
browser_tests/assets/execution/parallel_async_nodes.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 10,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [26, 200],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [1, 2, 3], "slot_index": 0 },
|
||||
{ "name": "MASK", "type": "MASK", "links": null, "slot_index": 1 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "LoadImage" },
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "TestSleep",
|
||||
"pos": [400, 100],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 1 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [4], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.0]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "TestSleep",
|
||||
"pos": [400, 250],
|
||||
"size": [210, 86],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 2 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [5], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestSleep" },
|
||||
"widgets_values": [2.5]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "TestAsyncProgressNode",
|
||||
"pos": [400, 400],
|
||||
"size": [210, 126],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "value", "type": "IMAGE", "link": 3 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [6], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestAsyncProgressNode" },
|
||||
"widgets_values": [3.0, 10]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "TestVariadicAverage",
|
||||
"pos": [700, 200],
|
||||
"size": [210, 106],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "input1", "type": "IMAGE", "link": 4 },
|
||||
{ "name": "input2", "type": "IMAGE", "link": 5 },
|
||||
{ "name": "input3", "type": "IMAGE", "link": 6 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": [7], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "TestVariadicAverage" }
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "SaveImage",
|
||||
"pos": [1000, 200],
|
||||
"size": [315, 270],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "images", "type": "IMAGE", "link": 7 }
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [700, 400],
|
||||
"size": [210, 54],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "clip", "type": "CLIP", "link": null }
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "CONDITIONING", "type": "CONDITIONING", "links": [], "slot_index": 0 }
|
||||
],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" },
|
||||
"widgets_values": ["test"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 2, 0, "IMAGE"],
|
||||
[2, 1, 0, 3, 0, "IMAGE"],
|
||||
[3, 1, 0, 4, 0, "IMAGE"],
|
||||
[4, 2, 0, 5, 0, "IMAGE"],
|
||||
[5, 3, 0, 5, 1, "IMAGE"],
|
||||
[6, 4, 0, 5, 2, "IMAGE"],
|
||||
[7, 5, 0, 6, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
193
browser_tests/assets/execution/parallel_ksamplers.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 13,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [26, 474],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{ "name": "MODEL", "type": "MODEL", "links": [1, 10], "slot_index": 0 },
|
||||
{ "name": "CLIP", "type": "CLIP", "links": [2, 3], "slot_index": 1 },
|
||||
{ "name": "VAE", "type": "VAE", "links": [8, 11], "slot_index": 2 }
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [415, 186],
|
||||
"size": [422.84503173828125, 164.31304931640625],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "clip", "type": "CLIP", "link": 2 }],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [4, 12],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [413, 389],
|
||||
"size": [425.27801513671875, 180.6060791015625],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "clip", "type": "CLIP", "link": 3 }],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [5, 13],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": ["text, watermark"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [473, 609],
|
||||
"size": [315, 106],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [6, 14], "slot_index": 0 }],
|
||||
"properties": {},
|
||||
"widgets_values": [512, 512, 1]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "KSampler",
|
||||
"pos": [863, 186],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": 1 },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": 4 },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": 5 },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": 6 }
|
||||
],
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [7], "slot_index": 0 }],
|
||||
"properties": {},
|
||||
"widgets_values": [156680208700286, true, 2, 8, "euler", "normal", 1]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1209, 188],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "samples", "type": "LATENT", "link": 7 },
|
||||
{ "name": "vae", "type": "VAE", "link": 8 }
|
||||
],
|
||||
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": [9], "slot_index": 0 }],
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "SaveImage",
|
||||
"pos": [1451, 189],
|
||||
"size": [210, 26],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "images", "type": "IMAGE", "link": 9 }],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "KSampler",
|
||||
"pos": [863, 486],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": 10 },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": 12 },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": 13 },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": 14 }
|
||||
],
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [15], "slot_index": 0 }],
|
||||
"properties": {},
|
||||
"widgets_values": [156680208700287, true, 3, 8, "euler", "normal", 1]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1209, 488],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "samples", "type": "LATENT", "link": 15 },
|
||||
{ "name": "vae", "type": "VAE", "link": 11 }
|
||||
],
|
||||
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": [16], "slot_index": 0 }],
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SaveImage",
|
||||
"pos": [1451, 489],
|
||||
"size": [210, 26],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [{ "name": "images", "type": "IMAGE", "link": 16 }],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 5, 0, "MODEL"],
|
||||
[2, 1, 1, 2, 0, "CLIP"],
|
||||
[3, 1, 1, 3, 0, "CLIP"],
|
||||
[4, 2, 0, 5, 1, "CONDITIONING"],
|
||||
[5, 3, 0, 5, 2, "CONDITIONING"],
|
||||
[6, 4, 0, 5, 3, "LATENT"],
|
||||
[7, 5, 0, 6, 0, "LATENT"],
|
||||
[8, 1, 2, 6, 1, "VAE"],
|
||||
[9, 6, 0, 7, 0, "IMAGE"],
|
||||
[10, 1, 0, 8, 0, "MODEL"],
|
||||
[11, 1, 2, 9, 1, "VAE"],
|
||||
[12, 2, 0, 8, 1, "CONDITIONING"],
|
||||
[13, 3, 0, 8, 2, "CONDITIONING"],
|
||||
[14, 4, 0, 8, 3, "LATENT"],
|
||||
[15, 8, 0, 9, 0, "LATENT"],
|
||||
[16, 9, 0, 10, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
716
browser_tests/assets/nested-subgraph.json
Normal file
@@ -0,0 +1,716 @@
|
||||
{
|
||||
"id": "976d6e9a-927d-42db-abd4-96bfc0ecf8d9",
|
||||
"revision": 0,
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 11,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "8beb610f-ddd1-4489-ae0d-2f732a4042ae",
|
||||
"pos": [
|
||||
532,
|
||||
412.5
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "subgraph 2",
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
758.2109985351562,
|
||||
398.3681335449219
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 10
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1028.9615478515625,
|
||||
381.83746337890625
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
9,
|
||||
8,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
11,
|
||||
10,
|
||||
1,
|
||||
8,
|
||||
1,
|
||||
"VAE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "8beb610f-ddd1-4489-ae0d-2f732a4042ae",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 10,
|
||||
"lastLinkId": 14,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "subgraph 2",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
-154,
|
||||
415.5,
|
||||
120,
|
||||
40
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
1238,
|
||||
395.5,
|
||||
120,
|
||||
80
|
||||
]
|
||||
},
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "4d6c7e4e-971e-4f78-9218-9a604db53a4b",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"linkIds": [
|
||||
7
|
||||
],
|
||||
"localized_name": "LATENT",
|
||||
"pos": {
|
||||
"0": 1258,
|
||||
"1": 415.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f8201d4f-7fc6-4a1b-b8c9-9f0716d9c09a",
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"linkIds": [
|
||||
14
|
||||
],
|
||||
"localized_name": "VAE",
|
||||
"pos": {
|
||||
"0": 1258,
|
||||
"1": 435.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
415,
|
||||
186
|
||||
],
|
||||
"size": [
|
||||
422.84503173828125,
|
||||
164.31304931640625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
863,
|
||||
186
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "model",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 12
|
||||
},
|
||||
{
|
||||
"localized_name": "positive",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"localized_name": "negative",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 10
|
||||
},
|
||||
{
|
||||
"localized_name": "latent_image",
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
32115495257102,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "dbe5763f-440b-47b4-82ac-454f1f98b0e3",
|
||||
"pos": [
|
||||
194.13900756835938,
|
||||
657.3333740234375
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "MODEL",
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "CLIP",
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "VAE",
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
14
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "subgraph 3",
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 1,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "LATENT"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": 10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 2,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"origin_id": 10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 3,
|
||||
"target_slot": 3,
|
||||
"type": "LATENT"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"origin_id": 10,
|
||||
"origin_slot": 2,
|
||||
"target_id": 3,
|
||||
"target_slot": 0,
|
||||
"type": "MODEL"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"origin_id": 10,
|
||||
"origin_slot": 3,
|
||||
"target_id": 6,
|
||||
"target_slot": 0,
|
||||
"type": "CLIP"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"origin_id": 10,
|
||||
"origin_slot": 4,
|
||||
"target_id": -20,
|
||||
"target_slot": 1,
|
||||
"type": "VAE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "dbe5763f-440b-47b4-82ac-454f1f98b0e3",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 9,
|
||||
"lastLinkId": 9,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "subgraph 3",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
-154,
|
||||
517,
|
||||
120,
|
||||
40
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
898.2780151367188,
|
||||
467,
|
||||
128.6640625,
|
||||
140
|
||||
]
|
||||
},
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "b4882169-329b-43f6-a373-81abfbdea55b",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"linkIds": [
|
||||
6
|
||||
],
|
||||
"localized_name": "CONDITIONING",
|
||||
"pos": {
|
||||
"0": 918.2780151367188,
|
||||
"1": 487
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "01f51f96-a741-428e-8772-9557ee50b609",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"linkIds": [
|
||||
2
|
||||
],
|
||||
"localized_name": "LATENT",
|
||||
"pos": {
|
||||
"0": 918.2780151367188,
|
||||
"1": 507
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47fa906e-d80b-45c3-a596-211a0e59d4a1",
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"linkIds": [
|
||||
1
|
||||
],
|
||||
"localized_name": "MODEL",
|
||||
"pos": {
|
||||
"0": 918.2780151367188,
|
||||
"1": 527
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f03dccd7-10e8-4513-9994-15854a92d192",
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"linkIds": [
|
||||
3
|
||||
],
|
||||
"localized_name": "CLIP",
|
||||
"pos": {
|
||||
"0": 918.2780151367188,
|
||||
"1": 547
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a666877f-e34f-49bc-8a78-b26156656b83",
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"linkIds": [
|
||||
8
|
||||
],
|
||||
"localized_name": "VAE",
|
||||
"pos": {
|
||||
"0": 918.2780151367188,
|
||||
"1": 567
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
413,
|
||||
389
|
||||
],
|
||||
"size": [
|
||||
425.27801513671875,
|
||||
180.6060791015625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"text, watermark"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
473,
|
||||
609
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
26,
|
||||
474
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "MODEL",
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "CLIP",
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
3,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "VAE",
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly-fp16.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 5,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 1,
|
||||
"target_id": 7,
|
||||
"target_slot": 0,
|
||||
"type": "CLIP"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"origin_id": 7,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 5,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 1,
|
||||
"type": "LATENT"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 2,
|
||||
"type": "MODEL"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 1,
|
||||
"target_id": -20,
|
||||
"target_slot": 3,
|
||||
"type": "CLIP"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 2,
|
||||
"target_id": -20,
|
||||
"target_slot": 4,
|
||||
"type": "VAE"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.24.0-1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
507
browser_tests/helpers/ExecutionTestHelper.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
export interface ExecutionEventTracker {
|
||||
progressStates: any[]
|
||||
executionStarted: boolean
|
||||
executionFinished: boolean
|
||||
executionError: any | null
|
||||
executingNodeId: string | null
|
||||
}
|
||||
|
||||
export interface ProgressState {
|
||||
prompt_id: string
|
||||
nodes: Record<
|
||||
string,
|
||||
{
|
||||
state: 'running' | 'finished' | 'waiting'
|
||||
node_id: string
|
||||
display_node_id: string
|
||||
prompt_id: string
|
||||
value?: number
|
||||
max?: number
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export class ExecutionTestHelper {
|
||||
private testId: string
|
||||
|
||||
constructor(private page: Page) {
|
||||
// Generate unique ID for this test instance to avoid conflicts
|
||||
this.testId = `test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up common event tracking for execution tests
|
||||
*/
|
||||
async setupEventTracking(): Promise<void> {
|
||||
await this.page.evaluate((testId) => {
|
||||
// Use unique property names for this test instance
|
||||
const progressKey = `__progressStates_${testId}`
|
||||
const startedKey = `__executionStarted_${testId}`
|
||||
const finishedKey = `__executionFinished_${testId}`
|
||||
const errorKey = `__executionError_${testId}`
|
||||
const nodeIdKey = `__executingNodeId_${testId}`
|
||||
|
||||
window[progressKey] = []
|
||||
window[startedKey] = false
|
||||
window[finishedKey] = false
|
||||
window[errorKey] = null
|
||||
window[nodeIdKey] = null
|
||||
|
||||
const api = window['app'].api
|
||||
|
||||
// Store listeners so we can clean them up later
|
||||
if (!window['__testListeners']) {
|
||||
window['__testListeners'] = {}
|
||||
}
|
||||
|
||||
// Remove old listeners if they exist
|
||||
if (window['__testListeners'][testId]) {
|
||||
const oldListeners = window['__testListeners'][testId]
|
||||
api.removeEventListener('progress_state', oldListeners.progress)
|
||||
api.removeEventListener('executing', oldListeners.executing)
|
||||
api.removeEventListener('execution_error', oldListeners.error)
|
||||
}
|
||||
|
||||
// Create new listeners
|
||||
const progressListener = (event) => {
|
||||
window[progressKey].push(event.detail)
|
||||
}
|
||||
|
||||
const executingListener = (event) => {
|
||||
window[nodeIdKey] = event.detail
|
||||
if (event.detail !== null) {
|
||||
window[startedKey] = true
|
||||
} else {
|
||||
window[finishedKey] = true
|
||||
}
|
||||
}
|
||||
|
||||
const errorListener = (event) => {
|
||||
window[errorKey] = event.detail
|
||||
}
|
||||
|
||||
// Add listeners
|
||||
api.addEventListener('progress_state', progressListener)
|
||||
api.addEventListener('executing', executingListener)
|
||||
api.addEventListener('execution_error', errorListener)
|
||||
|
||||
// Store listeners for cleanup
|
||||
window['__testListeners'][testId] = {
|
||||
progress: progressListener,
|
||||
executing: executingListener,
|
||||
error: errorListener
|
||||
}
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current event tracking state
|
||||
*/
|
||||
async getEventState(): Promise<ExecutionEventTracker> {
|
||||
return await this.page.evaluate(
|
||||
(testId) => ({
|
||||
progressStates: window[`__progressStates_${testId}`] || [],
|
||||
executionStarted: window[`__executionStarted_${testId}`] || false,
|
||||
executionFinished: window[`__executionFinished_${testId}`] || false,
|
||||
executionError: window[`__executionError_${testId}`] || null,
|
||||
executingNodeId: window[`__executingNodeId_${testId}`] || null
|
||||
}),
|
||||
this.testId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for execution to start
|
||||
*/
|
||||
async waitForExecutionStart(timeout: number = 10000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(testId) => window[`__executionStarted_${testId}`] === true,
|
||||
this.testId,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for execution to finish
|
||||
*/
|
||||
async waitForExecutionFinish(timeout: number = 30000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(testId) => window[`__executionFinished_${testId}`] === true,
|
||||
this.testId,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specific number of nodes to be running
|
||||
*/
|
||||
async waitForRunningNodes(
|
||||
count: number,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
({ expectedCount, testId }) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running'
|
||||
).length
|
||||
|
||||
return runningNodes >= expectedCount
|
||||
},
|
||||
{ expectedCount: count, testId: this.testId },
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for at least one node to finish
|
||||
*/
|
||||
async waitForNodeFinish(timeout: number = 15000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
return Object.values(latestState.nodes).some(
|
||||
(node: any) => node.state === 'finished'
|
||||
)
|
||||
},
|
||||
this.testId,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest progress state
|
||||
*/
|
||||
async getLatestProgressState(): Promise<ProgressState | null> {
|
||||
return await this.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
return states[states.length - 1]
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for node progress to be applied to the graph
|
||||
*/
|
||||
async waitForGraphNodeProgress(
|
||||
nodeIds: number[],
|
||||
timeout: number = 5000
|
||||
): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
(ids) => {
|
||||
return ids.some((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node?.progress !== undefined && node.progress >= 0
|
||||
})
|
||||
},
|
||||
nodeIds,
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets node progress from the graph
|
||||
*/
|
||||
async getGraphNodeProgress(nodeId: number): Promise<number | undefined> {
|
||||
return await this.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node?.progress
|
||||
}, nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if execution had errors
|
||||
*/
|
||||
async hasExecutionError(): Promise<boolean> {
|
||||
const error = await this.page.evaluate(
|
||||
(testId) => window[`__executionError_${testId}`],
|
||||
this.testId
|
||||
)
|
||||
return error !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets execution error details
|
||||
*/
|
||||
async getExecutionError(): Promise<any> {
|
||||
return await this.page.evaluate(
|
||||
(testId) => window[`__executionError_${testId}`],
|
||||
this.testId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup event listeners when test is done
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
await this.page.evaluate((testId) => {
|
||||
if (window['__testListeners'] && window['__testListeners'][testId]) {
|
||||
const api = window['app'].api
|
||||
const listeners = window['__testListeners'][testId]
|
||||
api.removeEventListener('progress_state', listeners.progress)
|
||||
api.removeEventListener('executing', listeners.executing)
|
||||
api.removeEventListener('execution_error', listeners.error)
|
||||
delete window['__testListeners'][testId]
|
||||
}
|
||||
// Clean up test-specific properties
|
||||
delete window[`__progressStates_${testId}`]
|
||||
delete window[`__executionStarted_${testId}`]
|
||||
delete window[`__executionFinished_${testId}`]
|
||||
delete window[`__executionError_${testId}`]
|
||||
delete window[`__executingNodeId_${testId}`]
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the testId for direct window access in evaluate functions
|
||||
*/
|
||||
getTestId(): string {
|
||||
return this.testId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for browser title monitoring
|
||||
*/
|
||||
export class BrowserTitleMonitor {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Waits for title to not show execution state
|
||||
*/
|
||||
async waitForIdleTitle(timeout: number = 10000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
return !title.match(/\[\d+%\]/) && !title.match(/\[\d+ nodes running\]/)
|
||||
},
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for title to show execution state
|
||||
*/
|
||||
async waitForExecutionTitle(timeout: number = 5000): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
return title.match(/\[\d+%\]/) || title.match(/\[\d+ nodes running\]/)
|
||||
},
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up title change monitoring
|
||||
*/
|
||||
async setupTitleMonitoring(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
window['__titleUpdateLog'] = []
|
||||
window['__lastTitle'] = document.title
|
||||
|
||||
window['__titleInterval'] = setInterval(() => {
|
||||
const newTitle = document.title
|
||||
if (newTitle !== window['__lastTitle']) {
|
||||
window['__titleUpdateLog'].push({
|
||||
time: Date.now(),
|
||||
title: newTitle,
|
||||
hasProgress: !!newTitle.match(/\[\d+%\]/),
|
||||
hasMultiNode: !!newTitle.match(/\[\d+ nodes running\]/)
|
||||
})
|
||||
window['__lastTitle'] = newTitle
|
||||
}
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops title monitoring and returns the log
|
||||
*/
|
||||
async stopTitleMonitoring(): Promise<any[]> {
|
||||
const log = await this.page.evaluate(() => {
|
||||
if (window['__titleInterval']) {
|
||||
clearInterval(window['__titleInterval'])
|
||||
}
|
||||
return window['__titleUpdateLog'] || []
|
||||
})
|
||||
return log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for preview event handling
|
||||
*/
|
||||
export class PreviewTestHelper {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Sets up preview event tracking
|
||||
*/
|
||||
async setupPreviewTracking(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
window['__previewEvents'] = []
|
||||
window['__revokedNodes'] = []
|
||||
window['__revokedUrls'] = []
|
||||
|
||||
// Track preview events
|
||||
const api = window['app'].api
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
window['__previewEvents'].push({
|
||||
nodeId: event.detail.nodeId,
|
||||
displayNodeId: event.detail.displayNodeId,
|
||||
parentNodeId: event.detail.parentNodeId,
|
||||
realNodeId: event.detail.realNodeId,
|
||||
promptId: event.detail.promptId
|
||||
})
|
||||
})
|
||||
|
||||
// Mock revokePreviews to track calls
|
||||
const originalRevoke = window['app'].revokePreviews
|
||||
window['app'].revokePreviews = function (nodeId) {
|
||||
window['__revokedNodes'].push(nodeId)
|
||||
originalRevoke.call(this, nodeId)
|
||||
}
|
||||
|
||||
// Mock URL.revokeObjectURL to track URL revocations
|
||||
const originalRevokeURL = URL.revokeObjectURL
|
||||
URL.revokeObjectURL = (url: string) => {
|
||||
window['__revokedUrls'].push(url)
|
||||
originalRevokeURL.call(URL, url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tracked preview events
|
||||
*/
|
||||
async getPreviewEvents(): Promise<any[]> {
|
||||
return await this.page.evaluate(() => window['__previewEvents'] || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets revoked node IDs
|
||||
*/
|
||||
async getRevokedNodes(): Promise<string[]> {
|
||||
return await this.page.evaluate(() => window['__revokedNodes'] || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets revoked URLs
|
||||
*/
|
||||
async getRevokedUrls(): Promise<string[]> {
|
||||
return await this.page.evaluate(() => window['__revokedUrls'] || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets fake preview for a node
|
||||
*/
|
||||
async setNodePreview(nodeId: string, previewUrl: string): Promise<void> {
|
||||
await this.page.evaluate(
|
||||
({ id, url }) => {
|
||||
window['app'].nodePreviewImages[id] = [url]
|
||||
},
|
||||
{ id: nodeId, url: previewUrl }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets node preview URLs
|
||||
*/
|
||||
async getNodePreviews(nodeId: string): Promise<string[] | undefined> {
|
||||
return await this.page.evaluate(
|
||||
(id) => window['app'].nodePreviewImages[id],
|
||||
nodeId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for checking subgraph execution
|
||||
*/
|
||||
export class SubgraphTestHelper {
|
||||
private testId: string
|
||||
|
||||
constructor(private page: Page) {
|
||||
// Generate unique ID for this test instance
|
||||
this.testId = `test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the test ID to match ExecutionTestHelper
|
||||
*/
|
||||
setTestId(testId: string): void {
|
||||
this.testId = testId
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for nested node progress (nodes with ':' in their ID)
|
||||
*/
|
||||
async waitForNestedNodeProgress(
|
||||
minNestingLevel: number = 1,
|
||||
timeout: number = 15000
|
||||
): Promise<void> {
|
||||
await this.page.waitForFunction(
|
||||
({ minLevel, testId }) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
return states.some((state: any) => {
|
||||
if (!state.nodes) return false
|
||||
return Object.keys(state.nodes).some((nodeId) => {
|
||||
const colonCount = (nodeId.match(/:/g) || []).length
|
||||
return colonCount >= minLevel
|
||||
})
|
||||
})
|
||||
},
|
||||
{ minLevel: minNestingLevel, testId: this.testId },
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all nested node IDs from progress states
|
||||
*/
|
||||
async getNestedNodeIds(): Promise<string[]> {
|
||||
return await this.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`] || []
|
||||
const nestedIds = new Set<string>()
|
||||
|
||||
states.forEach((state: any) => {
|
||||
if (state.nodes) {
|
||||
Object.keys(state.nodes).forEach((nodeId) => {
|
||||
if (nodeId.includes(':')) {
|
||||
nestedIds.add(nodeId)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(nestedIds)
|
||||
}, this.testId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node has running stroke style
|
||||
*/
|
||||
async hasRunningStrokeStyle(nodeId: number): Promise<boolean> {
|
||||
return await this.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node?.strokeStyles?.['running']) return false
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color === '#0f0'
|
||||
}, nodeId)
|
||||
}
|
||||
}
|
||||
316
browser_tests/tests/browserTabTitleMultiNode.spec.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
BrowserTitleMonitor,
|
||||
ExecutionTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Browser Tab Title - Multi-node Execution', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let titleMonitor: BrowserTitleMonitor
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
titleMonitor = new BrowserTitleMonitor(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
// Clean up event listeners to avoid conflicts
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Shows multiple nodes running in tab title', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle()
|
||||
|
||||
// Get initial title
|
||||
const initialTitle = await comfyPage.page.title()
|
||||
// Title might show execution state if other tests are running
|
||||
// Just ensure we have a baseline to compare against
|
||||
|
||||
// Wait for the UI to be ready
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check if workflow is valid and nodes are available
|
||||
const workflowStatus = await comfyPage.page.evaluate(() => {
|
||||
const graph = window['app'].graph
|
||||
const missingNodeTypes: string[] = []
|
||||
const nodeCount = graph.nodes.length
|
||||
|
||||
// Check for missing node types
|
||||
graph.nodes.forEach((node: any) => {
|
||||
if (node.type && !LiteGraph.registered_node_types[node.type]) {
|
||||
missingNodeTypes.push(node.type)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
nodeCount,
|
||||
missingNodeTypes,
|
||||
hasErrors: missingNodeTypes.length > 0
|
||||
}
|
||||
})
|
||||
|
||||
if (workflowStatus.hasErrors) {
|
||||
console.log('Missing node types:', workflowStatus.missingNodeTypes)
|
||||
// Skip test if nodes are missing
|
||||
test.skip()
|
||||
return
|
||||
}
|
||||
|
||||
// Set up tracking for progress events and errors
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Queue the workflow for real execution using the command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait a moment to see if there's an error
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Check for execution errors
|
||||
if (await executionHelper.hasExecutionError()) {
|
||||
const error = await executionHelper.getExecutionError()
|
||||
console.log('Execution error:', error)
|
||||
}
|
||||
|
||||
// Wait for multiple nodes to be running (TestSleep nodes 2, 3 and TestAsyncProgressNode 4)
|
||||
await executionHelper.waitForRunningNodes(2)
|
||||
|
||||
// Check title while we know multiple nodes are running
|
||||
const testId = executionHelper.getTestId()
|
||||
const titleDuringExecution = await comfyPage.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return null
|
||||
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running'
|
||||
).length
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
runningCount: runningNodes
|
||||
}
|
||||
}, testId)
|
||||
|
||||
// Verify we captured the state with multiple nodes running
|
||||
expect(titleDuringExecution).not.toBeNull()
|
||||
expect(titleDuringExecution.runningCount).toBeGreaterThanOrEqual(2)
|
||||
|
||||
// The title should show multiple nodes running when we have 2+ nodes executing
|
||||
if (titleDuringExecution.runningCount >= 2) {
|
||||
expect(titleDuringExecution.title).toMatch(/\[\d+ nodes running\]/)
|
||||
}
|
||||
|
||||
// Wait for some nodes to finish, leaving only one running
|
||||
await executionHelper.waitForRunningNodes(1, 15000)
|
||||
|
||||
// Wait for title to show single node progress
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
return title.match(/\[\d+%\]/) && !title.match(/\[\d+ nodes running\]/)
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that title shows single node with progress
|
||||
const titleWithSingleNode = await comfyPage.page.title()
|
||||
expect(titleWithSingleNode).toMatch(/\[\d+%\]/)
|
||||
expect(titleWithSingleNode).not.toMatch(/\[\d+ nodes running\]/)
|
||||
})
|
||||
|
||||
test('Shows progress updates in title during execution', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for the UI to be ready
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Set up tracking for progress events and title changes
|
||||
await executionHelper.setupEventTracking()
|
||||
await titleMonitor.setupTitleMonitoring()
|
||||
|
||||
// Queue the workflow for real execution using the command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for TestAsyncProgressNode (node 4) to start showing progress
|
||||
// This node reports progress from 0 to 10 with steps of 1
|
||||
const testId2 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes || !latestState.nodes['4']) return false
|
||||
|
||||
const node4 = latestState.nodes['4']
|
||||
if (node4.state === 'running' && node4.value > 0) {
|
||||
window['__lastProgress'] = Math.round((node4.value / node4.max) * 100)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
testId2,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for title to show progress percentage
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const title = document.title
|
||||
console.log('Title check 1:', title)
|
||||
return title.match(/\[\d+%\]/)
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that title shows a progress percentage
|
||||
const titleWithProgress = await comfyPage.page.title()
|
||||
expect(titleWithProgress).toMatch(/\[\d+%\]/)
|
||||
|
||||
// Wait for progress to update to a different value
|
||||
const firstProgress = await comfyPage.page.evaluate(
|
||||
() => window['__lastProgress']
|
||||
)
|
||||
|
||||
const testId3 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
({ initialProgress, testId }) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes || !latestState.nodes['4']) return false
|
||||
|
||||
const node4 = latestState.nodes['4']
|
||||
if (node4.state === 'running') {
|
||||
const currentProgress = Math.round((node4.value / node4.max) * 100)
|
||||
window['__lastProgress'] = currentProgress
|
||||
return currentProgress > initialProgress
|
||||
}
|
||||
return false
|
||||
},
|
||||
{ initialProgress: firstProgress, testId: testId3 },
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Store the first progress for comparison
|
||||
await comfyPage.page.evaluate((progress) => {
|
||||
window['__firstProgress'] = progress
|
||||
}, firstProgress)
|
||||
|
||||
// Check the title history to verify we captured progress updates
|
||||
const finalCheck = await comfyPage.page.evaluate(() => {
|
||||
const titleLog = window['__titleUpdateLog'] || []
|
||||
const firstProgress = window['__firstProgress'] || 0
|
||||
|
||||
// Find titles with progress information
|
||||
const titlesWithProgress = titleLog.filter((entry) => entry.hasProgress)
|
||||
|
||||
// Check if we saw different progress values or multi-node running state
|
||||
const progressValues = new Set()
|
||||
const hadMultiNodeRunning = titleLog.some((entry) =>
|
||||
entry.title.includes('nodes running')
|
||||
)
|
||||
|
||||
titleLog.forEach((entry) => {
|
||||
const match = entry.title.match(/\[(\d+)%\]/)
|
||||
if (match) {
|
||||
progressValues.add(parseInt(match[1]))
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
sawProgressUpdates: titlesWithProgress.length > 0,
|
||||
uniqueProgressValues: Array.from(progressValues),
|
||||
hadMultiNodeRunning,
|
||||
firstProgress,
|
||||
lastProgress: window['__lastProgress'],
|
||||
totalTitleUpdates: titleLog.length,
|
||||
sampleTitles: titleLog.slice(0, 5)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Title update check:', JSON.stringify(finalCheck, null, 2))
|
||||
|
||||
// Verify that we captured title updates showing execution progress
|
||||
expect(finalCheck.sawProgressUpdates).toBe(true)
|
||||
expect(finalCheck.totalTitleUpdates).toBeGreaterThan(0)
|
||||
|
||||
// We should have seen either:
|
||||
// 1. Multiple unique progress values, OR
|
||||
// 2. Multi-node running state, OR
|
||||
// 3. Progress different from initial
|
||||
const sawProgressChange =
|
||||
finalCheck.uniqueProgressValues.length > 1 ||
|
||||
finalCheck.hadMultiNodeRunning ||
|
||||
finalCheck.lastProgress !== firstProgress
|
||||
|
||||
expect(sawProgressChange).toBe(true)
|
||||
|
||||
// Clean up interval
|
||||
await titleMonitor.stopTitleMonitoring()
|
||||
})
|
||||
|
||||
test('Clears execution status from title when all nodes finish', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle()
|
||||
|
||||
// Wait for the UI to be ready
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Set up tracking for events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Queue the workflow for real execution using the command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to show progress in title
|
||||
await titleMonitor.waitForExecutionTitle()
|
||||
|
||||
// Verify execution shows in title
|
||||
const executingTitle = await comfyPage.page.title()
|
||||
expect(executingTitle).toMatch(/\[[\d%\s\w]+\]/)
|
||||
|
||||
// Wait for execution to complete (all nodes finished)
|
||||
await executionHelper.waitForExecutionFinish()
|
||||
|
||||
// Give a moment for title to update after execution completes
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Wait for title to clear execution status
|
||||
await titleMonitor.waitForIdleTitle()
|
||||
|
||||
// Check that execution status is cleared
|
||||
const finishedTitle = await comfyPage.page.title()
|
||||
expect(finishedTitle).toContain('ComfyUI')
|
||||
expect(finishedTitle).not.toMatch(/\[\d+%\]/) // No percentage
|
||||
expect(finishedTitle).not.toMatch(/\[\d+ nodes running\]/) // No running nodes
|
||||
expect(finishedTitle).not.toContain('Executing')
|
||||
})
|
||||
})
|
||||
85
browser_tests/tests/browserTabTitleSimple.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
BrowserTitleMonitor,
|
||||
ExecutionTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
test.describe('Browser Tab Title - Multi-node Simple', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let titleMonitor: BrowserTitleMonitor
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
titleMonitor = new BrowserTitleMonitor(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Title updates based on execution state', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle().catch(() => {
|
||||
// If timeout, cancel any running execution
|
||||
return comfyPage.page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
// Get initial title
|
||||
const initialTitle = await comfyPage.page.title()
|
||||
// Title might show execution state if other tests are running
|
||||
// Just ensure we can detect when it changes
|
||||
const hasExecutionState =
|
||||
initialTitle.match(/\[\d+%\]/) ||
|
||||
initialTitle.match(/\[\d+ nodes running\]/)
|
||||
|
||||
// Set up tracking for execution events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command instead of button
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for title to update with execution state
|
||||
await titleMonitor.waitForExecutionTitle()
|
||||
|
||||
const executingTitle = await comfyPage.page.title()
|
||||
// If initial title didn't have execution state, it should be different now
|
||||
if (!hasExecutionState) {
|
||||
expect(executingTitle).not.toBe(initialTitle)
|
||||
}
|
||||
expect(executingTitle).toMatch(/\[[\d%\s\w]+\]/)
|
||||
})
|
||||
|
||||
test('Can read workflow name from title', async ({ comfyPage }) => {
|
||||
// Wait for any existing execution to complete
|
||||
await titleMonitor.waitForIdleTitle(5000).catch(async () => {
|
||||
// Cancel any running execution
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
})
|
||||
|
||||
// Set a workflow name
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.workflow.activeWorkflow.filename =
|
||||
'test-workflow'
|
||||
})
|
||||
|
||||
// Wait for title to update
|
||||
await comfyPage.page.waitForTimeout(100)
|
||||
|
||||
const title = await comfyPage.page.title()
|
||||
expect(title).toContain('test-workflow')
|
||||
// Title should contain workflow name regardless of execution state
|
||||
})
|
||||
})
|
||||
382
browser_tests/tests/featureFlags.spec.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Feature Flags', () => {
|
||||
test('Client and server exchange feature flags on connection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Navigate to a new page to capture the initial WebSocket connection
|
||||
const newPage = await comfyPage.page.context().newPage()
|
||||
|
||||
// Set up monitoring before navigation
|
||||
await newPage.addInitScript(() => {
|
||||
// This runs before any page scripts
|
||||
window.__capturedMessages = {
|
||||
clientFeatureFlags: null,
|
||||
serverFeatureFlags: null
|
||||
}
|
||||
|
||||
// Capture outgoing client messages
|
||||
const originalSend = WebSocket.prototype.send
|
||||
WebSocket.prototype.send = function (data) {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
if (parsed.type === 'feature_flags') {
|
||||
window.__capturedMessages.clientFeatureFlags = parsed
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, ignore
|
||||
}
|
||||
return originalSend.call(this, data)
|
||||
}
|
||||
|
||||
// Monitor for server feature flags
|
||||
const checkInterval = setInterval(() => {
|
||||
if (
|
||||
window['app']?.api?.serverFeatureFlags &&
|
||||
Object.keys(window['app'].api.serverFeatureFlags).length > 0
|
||||
) {
|
||||
window.__capturedMessages.serverFeatureFlags =
|
||||
window['app'].api.serverFeatureFlags
|
||||
clearInterval(checkInterval)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
// Clear after 10 seconds
|
||||
setTimeout(() => clearInterval(checkInterval), 10000)
|
||||
})
|
||||
|
||||
// Navigate to the app
|
||||
await newPage.goto(comfyPage.url)
|
||||
|
||||
// Wait for both client and server feature flags
|
||||
await newPage.waitForFunction(
|
||||
() =>
|
||||
window.__capturedMessages.clientFeatureFlags !== null &&
|
||||
window.__capturedMessages.serverFeatureFlags !== null,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Get the captured messages
|
||||
const messages = await newPage.evaluate(() => window.__capturedMessages)
|
||||
|
||||
// Verify client sent feature flags
|
||||
expect(messages.clientFeatureFlags).toBeTruthy()
|
||||
expect(messages.clientFeatureFlags).toHaveProperty('type', 'feature_flags')
|
||||
expect(messages.clientFeatureFlags).toHaveProperty('data')
|
||||
expect(messages.clientFeatureFlags.data).toHaveProperty(
|
||||
'supports_preview_metadata'
|
||||
)
|
||||
expect(
|
||||
typeof messages.clientFeatureFlags.data.supports_preview_metadata
|
||||
).toBe('boolean')
|
||||
|
||||
// Verify server sent feature flags back
|
||||
expect(messages.serverFeatureFlags).toBeTruthy()
|
||||
expect(messages.serverFeatureFlags).toHaveProperty(
|
||||
'supports_preview_metadata'
|
||||
)
|
||||
expect(typeof messages.serverFeatureFlags.supports_preview_metadata).toBe(
|
||||
'boolean'
|
||||
)
|
||||
expect(messages.serverFeatureFlags).toHaveProperty('max_upload_size')
|
||||
expect(typeof messages.serverFeatureFlags.max_upload_size).toBe('number')
|
||||
expect(Object.keys(messages.serverFeatureFlags).length).toBeGreaterThan(0)
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
|
||||
test('Server feature flags are received and accessible', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection to establish
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Get the actual server feature flags from the backend
|
||||
const serverFlags = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.serverFeatureFlags
|
||||
})
|
||||
|
||||
// Verify we received real feature flags from the backend
|
||||
expect(serverFlags).toBeTruthy()
|
||||
expect(Object.keys(serverFlags).length).toBeGreaterThan(0)
|
||||
|
||||
// The backend should send feature flags
|
||||
expect(serverFlags).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof serverFlags.supports_preview_metadata).toBe('boolean')
|
||||
expect(serverFlags).toHaveProperty('max_upload_size')
|
||||
expect(typeof serverFlags.max_upload_size).toBe('number')
|
||||
})
|
||||
|
||||
test('serverSupportsFeature method works with real backend flags', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Test serverSupportsFeature with real backend flags
|
||||
const supportsPreviewMetadata = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.serverSupportsFeature(
|
||||
'supports_preview_metadata'
|
||||
)
|
||||
})
|
||||
// The method should return a boolean based on the backend's value
|
||||
expect(typeof supportsPreviewMetadata).toBe('boolean')
|
||||
|
||||
// Test non-existent feature - should always return false
|
||||
const supportsNonExistent = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.serverSupportsFeature('non_existent_feature_xyz')
|
||||
})
|
||||
expect(supportsNonExistent).toBe(false)
|
||||
|
||||
// Test that the method only returns true for boolean true values
|
||||
const testResults = await comfyPage.page.evaluate(() => {
|
||||
// Temporarily modify serverFeatureFlags to test behavior
|
||||
const original = window['app'].api.serverFeatureFlags
|
||||
window['app'].api.serverFeatureFlags = {
|
||||
bool_true: true,
|
||||
bool_false: false,
|
||||
string_value: 'yes',
|
||||
number_value: 1,
|
||||
null_value: null
|
||||
}
|
||||
|
||||
const results = {
|
||||
bool_true: window['app'].api.serverSupportsFeature('bool_true'),
|
||||
bool_false: window['app'].api.serverSupportsFeature('bool_false'),
|
||||
string_value: window['app'].api.serverSupportsFeature('string_value'),
|
||||
number_value: window['app'].api.serverSupportsFeature('number_value'),
|
||||
null_value: window['app'].api.serverSupportsFeature('null_value')
|
||||
}
|
||||
|
||||
// Restore original
|
||||
window['app'].api.serverFeatureFlags = original
|
||||
return results
|
||||
})
|
||||
|
||||
// serverSupportsFeature should only return true for boolean true values
|
||||
expect(testResults.bool_true).toBe(true)
|
||||
expect(testResults.bool_false).toBe(false)
|
||||
expect(testResults.string_value).toBe(false)
|
||||
expect(testResults.number_value).toBe(false)
|
||||
expect(testResults.null_value).toBe(false)
|
||||
})
|
||||
|
||||
test('getServerFeature method works with real backend data', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Test getServerFeature method
|
||||
const previewMetadataValue = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeature('supports_preview_metadata')
|
||||
})
|
||||
expect(typeof previewMetadataValue).toBe('boolean')
|
||||
|
||||
// Test getting max_upload_size
|
||||
const maxUploadSize = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeature('max_upload_size')
|
||||
})
|
||||
expect(typeof maxUploadSize).toBe('number')
|
||||
expect(maxUploadSize).toBeGreaterThan(0)
|
||||
|
||||
// Test getServerFeature with default value for non-existent feature
|
||||
const defaultValue = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeature(
|
||||
'non_existent_feature_xyz',
|
||||
'default'
|
||||
)
|
||||
})
|
||||
expect(defaultValue).toBe('default')
|
||||
})
|
||||
|
||||
test('getServerFeatures returns all backend feature flags', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Test getServerFeatures returns all flags
|
||||
const allFeatures = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeatures()
|
||||
})
|
||||
|
||||
expect(allFeatures).toBeTruthy()
|
||||
expect(allFeatures).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof allFeatures.supports_preview_metadata).toBe('boolean')
|
||||
expect(allFeatures).toHaveProperty('max_upload_size')
|
||||
expect(Object.keys(allFeatures).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Client feature flags are immutable', async ({ comfyPage }) => {
|
||||
// Test that getClientFeatureFlags returns a copy
|
||||
const immutabilityTest = await comfyPage.page.evaluate(() => {
|
||||
const flags1 = window['app'].api.getClientFeatureFlags()
|
||||
const flags2 = window['app'].api.getClientFeatureFlags()
|
||||
|
||||
// Modify the first object
|
||||
flags1.test_modification = true
|
||||
|
||||
// Get flags again to check if original was modified
|
||||
const flags3 = window['app'].api.getClientFeatureFlags()
|
||||
|
||||
return {
|
||||
areEqual: flags1 === flags2,
|
||||
hasModification: flags3.test_modification !== undefined,
|
||||
hasSupportsPreview: flags1.supports_preview_metadata !== undefined,
|
||||
supportsPreviewValue: flags1.supports_preview_metadata
|
||||
}
|
||||
})
|
||||
|
||||
// Verify they are different objects (not the same reference)
|
||||
expect(immutabilityTest.areEqual).toBe(false)
|
||||
|
||||
// Verify modification didn't affect the original
|
||||
expect(immutabilityTest.hasModification).toBe(false)
|
||||
|
||||
// Verify the flags contain expected properties
|
||||
expect(immutabilityTest.hasSupportsPreview).toBe(true)
|
||||
expect(typeof immutabilityTest.supportsPreviewValue).toBe('boolean') // From clientFeatureFlags.json
|
||||
})
|
||||
|
||||
test('Server features are immutable when accessed via getServerFeatures', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection to establish
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
const immutabilityTest = await comfyPage.page.evaluate(() => {
|
||||
// Get a copy of server features
|
||||
const features1 = window['app'].api.getServerFeatures()
|
||||
|
||||
// Try to modify it
|
||||
features1.supports_preview_metadata = false
|
||||
features1.new_feature = 'added'
|
||||
|
||||
// Get another copy
|
||||
const features2 = window['app'].api.getServerFeatures()
|
||||
|
||||
return {
|
||||
modifiedValue: features1.supports_preview_metadata,
|
||||
originalValue: features2.supports_preview_metadata,
|
||||
hasNewFeature: features2.new_feature !== undefined,
|
||||
hasSupportsPreview: features2.supports_preview_metadata !== undefined
|
||||
}
|
||||
})
|
||||
|
||||
// The modification should only affect the copy
|
||||
expect(immutabilityTest.modifiedValue).toBe(false)
|
||||
expect(typeof immutabilityTest.originalValue).toBe('boolean') // Backend sends boolean for supports_preview_metadata
|
||||
expect(immutabilityTest.hasNewFeature).toBe(false)
|
||||
expect(immutabilityTest.hasSupportsPreview).toBe(true)
|
||||
})
|
||||
|
||||
test('Feature flags are negotiated early in connection lifecycle', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// This test verifies that feature flags are available early in the app lifecycle
|
||||
// which is important for protocol negotiation
|
||||
|
||||
// Create a new page to ensure clean state
|
||||
const newPage = await comfyPage.page.context().newPage()
|
||||
|
||||
// Set up monitoring before navigation
|
||||
await newPage.addInitScript(() => {
|
||||
// Track when various app components are ready
|
||||
;(window as any).__appReadiness = {
|
||||
featureFlagsReceived: false,
|
||||
apiInitialized: false,
|
||||
appInitialized: false
|
||||
}
|
||||
|
||||
// Monitor when feature flags arrive by checking periodically
|
||||
const checkFeatureFlags = setInterval(() => {
|
||||
if (
|
||||
window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !==
|
||||
undefined
|
||||
) {
|
||||
;(window as any).__appReadiness.featureFlagsReceived = true
|
||||
clearInterval(checkFeatureFlags)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
// Monitor API initialization
|
||||
const checkApi = setInterval(() => {
|
||||
if (window['app']?.api) {
|
||||
;(window as any).__appReadiness.apiInitialized = true
|
||||
clearInterval(checkApi)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
// Monitor app initialization
|
||||
const checkApp = setInterval(() => {
|
||||
if (window['app']?.graph) {
|
||||
;(window as any).__appReadiness.appInitialized = true
|
||||
clearInterval(checkApp)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
// Clean up after 10 seconds
|
||||
setTimeout(() => {
|
||||
clearInterval(checkFeatureFlags)
|
||||
clearInterval(checkApi)
|
||||
clearInterval(checkApp)
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
// Navigate to the app
|
||||
await newPage.goto(comfyPage.url)
|
||||
|
||||
// Wait for feature flags to be received
|
||||
await newPage.waitForFunction(
|
||||
() =>
|
||||
window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !==
|
||||
undefined,
|
||||
{
|
||||
timeout: 10000
|
||||
}
|
||||
)
|
||||
|
||||
// Get readiness state
|
||||
const readiness = await newPage.evaluate(() => {
|
||||
return {
|
||||
...(window as any).__appReadiness,
|
||||
currentFlags: window['app'].api.serverFeatureFlags
|
||||
}
|
||||
})
|
||||
|
||||
// Verify feature flags are available
|
||||
expect(readiness.currentFlags).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof readiness.currentFlags.supports_preview_metadata).toBe(
|
||||
'boolean'
|
||||
)
|
||||
expect(readiness.currentFlags).toHaveProperty('max_upload_size')
|
||||
|
||||
// Verify feature flags were received (we detected them via polling)
|
||||
expect(readiness.featureFlagsReceived).toBe(true)
|
||||
|
||||
// Verify API was initialized (feature flags require API)
|
||||
expect(readiness.apiInitialized).toBe(true)
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
|
||||
test('Backend /features endpoint returns feature flags', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Test the HTTP endpoint directly
|
||||
const response = await comfyPage.page.request.get(
|
||||
`${comfyPage.url}/api/features`
|
||||
)
|
||||
expect(response.ok()).toBe(true)
|
||||
|
||||
const features = await response.json()
|
||||
expect(features).toBeTruthy()
|
||||
expect(features).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof features.supports_preview_metadata).toBe('boolean')
|
||||
expect(features).toHaveProperty('max_upload_size')
|
||||
expect(Object.keys(features).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
385
browser_tests/tests/multiNodeExecution.spec.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
PreviewTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Multi-node Execution Progress', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let previewHelper: PreviewTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
previewHelper = new PreviewTestHelper(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Can track progress of multiple async nodes executing in parallel', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Get references to the async nodes
|
||||
const sleepNode1 = await comfyPage.getNodeRefById(2)
|
||||
const sleepNode2 = await comfyPage.getNodeRefById(3)
|
||||
const progressNode = await comfyPage.getNodeRefById(4)
|
||||
|
||||
// Verify nodes are present
|
||||
expect(sleepNode1).toBeDefined()
|
||||
expect(sleepNode2).toBeDefined()
|
||||
expect(progressNode).toBeDefined()
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for all three nodes (2, 3, 4) to show progress from real execution
|
||||
const testId = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
const node2 = latestState.nodes['2']
|
||||
const node3 = latestState.nodes['3']
|
||||
const node4 = latestState.nodes['4']
|
||||
|
||||
// Check that all nodes have started executing
|
||||
return (
|
||||
node2 &&
|
||||
node2.state === 'running' &&
|
||||
node3 &&
|
||||
node3.state === 'running' &&
|
||||
node4 &&
|
||||
node4.state === 'running'
|
||||
)
|
||||
},
|
||||
testId,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for progress to be applied to all nodes in the graph
|
||||
await executionHelper.waitForGraphNodeProgress([2, 3, 4])
|
||||
|
||||
// Check that all nodes show progress
|
||||
const nodeProgress1 = await sleepNode1.getProperty('progress')
|
||||
const nodeProgress2 = await sleepNode2.getProperty('progress')
|
||||
const nodeProgress3 = await progressNode.getProperty('progress')
|
||||
|
||||
// Progress values should now be defined (exact values depend on timing)
|
||||
expect(nodeProgress1).toBeDefined()
|
||||
expect(nodeProgress2).toBeDefined()
|
||||
expect(nodeProgress3).toBeDefined()
|
||||
expect(nodeProgress1).toBeGreaterThanOrEqual(0)
|
||||
expect(nodeProgress1).toBeLessThanOrEqual(1)
|
||||
expect(nodeProgress2).toBeGreaterThanOrEqual(0)
|
||||
expect(nodeProgress2).toBeLessThanOrEqual(1)
|
||||
expect(nodeProgress3).toBeGreaterThanOrEqual(0)
|
||||
expect(nodeProgress3).toBeLessThanOrEqual(1)
|
||||
|
||||
// Wait for at least one node to finish
|
||||
await executionHelper.waitForNodeFinish()
|
||||
|
||||
// Wait for the finished node's progress to be cleared
|
||||
const testId2 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
// Find which nodes are finished
|
||||
const finishedNodeIds = Object.entries(latestState.nodes)
|
||||
.filter(([_, node]: [string, any]) => node.state === 'finished')
|
||||
.map(([id, _]) => id)
|
||||
|
||||
// Check that finished nodes have no progress in the graph
|
||||
return finishedNodeIds.some((id) => {
|
||||
const node = window['app'].graph.getNodeById(parseInt(id))
|
||||
return node && node.progress === undefined
|
||||
})
|
||||
},
|
||||
testId2,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Get current state of nodes
|
||||
const testId3 = executionHelper.getTestId()
|
||||
const currentState = await comfyPage.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
const latestState = states[states.length - 1]
|
||||
const graphNodes = {
|
||||
'2': window['app'].graph.getNodeById(2),
|
||||
'3': window['app'].graph.getNodeById(3),
|
||||
'4': window['app'].graph.getNodeById(4)
|
||||
}
|
||||
|
||||
return {
|
||||
stateNodes: latestState.nodes,
|
||||
graphProgress: {
|
||||
'2': graphNodes['2']?.progress,
|
||||
'3': graphNodes['3']?.progress,
|
||||
'4': graphNodes['4']?.progress
|
||||
}
|
||||
}
|
||||
}, testId3)
|
||||
|
||||
// Verify that finished nodes have no progress, running nodes have progress
|
||||
if (currentState && currentState.stateNodes) {
|
||||
Object.entries(currentState.stateNodes).forEach(
|
||||
([nodeId, nodeState]: [string, any]) => {
|
||||
const graphProgress = currentState.graphProgress[nodeId]
|
||||
if (nodeState.state === 'finished') {
|
||||
expect(graphProgress).toBeUndefined()
|
||||
} else if (nodeState.state === 'running') {
|
||||
expect(graphProgress).toBeDefined()
|
||||
expect(graphProgress).toBeGreaterThanOrEqual(0)
|
||||
expect(graphProgress).toBeLessThanOrEqual(1)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Clean up by canceling execution
|
||||
})
|
||||
|
||||
test('Updates visual state for multiple executing nodes', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Wait for the graph to be properly initialized
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
return window['app']?.graph?.nodes?.length > 0
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for multiple nodes to start executing
|
||||
await executionHelper.waitForRunningNodes(2)
|
||||
|
||||
// Wait for the progress to be applied to nodes
|
||||
await executionHelper.waitForGraphNodeProgress([2, 3])
|
||||
|
||||
// Verify that nodes have progress set (indicates they are executing)
|
||||
const nodeStates = await comfyPage.page.evaluate(() => {
|
||||
const node2 = window['app'].graph.getNodeById(2)
|
||||
const node3 = window['app'].graph.getNodeById(3)
|
||||
return {
|
||||
node2Progress: node2?.progress,
|
||||
node3Progress: node3?.progress,
|
||||
// Check if any nodes are marked as running by having progress
|
||||
hasRunningNodes:
|
||||
(node2?.progress !== undefined && node2?.progress >= 0) ||
|
||||
(node3?.progress !== undefined && node3?.progress >= 0)
|
||||
}
|
||||
})
|
||||
|
||||
expect(nodeStates.node2Progress).toBeDefined()
|
||||
expect(nodeStates.node3Progress).toBeDefined()
|
||||
expect(nodeStates.hasRunningNodes).toBe(true)
|
||||
|
||||
// Wait for at least one node to finish
|
||||
await executionHelper.waitForNodeFinish()
|
||||
|
||||
// Wait for progress updates to reflect the finished state
|
||||
const testId4 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
// Find nodes by their state
|
||||
const finishedNodes = Object.entries(latestState.nodes)
|
||||
.filter(([_, node]: [string, any]) => node.state === 'finished')
|
||||
.map(([id, _]) => parseInt(id))
|
||||
|
||||
const runningNodes = Object.entries(latestState.nodes)
|
||||
.filter(([_, node]: [string, any]) => node.state === 'running')
|
||||
.map(([id, _]) => parseInt(id))
|
||||
|
||||
// Check graph nodes match the state
|
||||
const allFinishedCorrect = finishedNodes.every((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node && node.progress === undefined
|
||||
})
|
||||
|
||||
const allRunningCorrect = runningNodes.every((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node && node.progress !== undefined && node.progress >= 0
|
||||
})
|
||||
|
||||
return (
|
||||
allFinishedCorrect && allRunningCorrect && finishedNodes.length > 0
|
||||
)
|
||||
},
|
||||
testId4,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Verify the final node states
|
||||
const testId5 = executionHelper.getTestId()
|
||||
const finalNodeStates = await comfyPage.page.evaluate((testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return null
|
||||
const latestState = states[states.length - 1]
|
||||
const node2 = window['app'].graph.getNodeById(2)
|
||||
const node3 = window['app'].graph.getNodeById(3)
|
||||
|
||||
return {
|
||||
node2State: latestState.nodes['2']?.state,
|
||||
node3State: latestState.nodes['3']?.state,
|
||||
node2Progress: node2?.progress,
|
||||
node3Progress: node3?.progress
|
||||
}
|
||||
}, testId5)
|
||||
|
||||
// Verify finished nodes have no progress, running nodes have progress
|
||||
if (finalNodeStates) {
|
||||
if (finalNodeStates.node2State === 'finished') {
|
||||
expect(finalNodeStates.node2Progress).toBeUndefined()
|
||||
} else if (finalNodeStates.node2State === 'running') {
|
||||
expect(finalNodeStates.node2Progress).toBeDefined()
|
||||
}
|
||||
|
||||
if (finalNodeStates.node3State === 'finished') {
|
||||
expect(finalNodeStates.node3Progress).toBeUndefined()
|
||||
} else if (finalNodeStates.node3State === 'running') {
|
||||
expect(finalNodeStates.node3Progress).toBeDefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Clears previews when nodes start executing', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Initialize tracking for revoked previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['__revokedNodes'] = []
|
||||
})
|
||||
|
||||
// Set up some fake previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].nodePreviewImages['2'] = ['fake-preview-url-1']
|
||||
window['app'].nodePreviewImages['3'] = ['fake-preview-url-2']
|
||||
})
|
||||
|
||||
// Verify previews exist
|
||||
const previewsBefore = await comfyPage.page.evaluate(() => {
|
||||
return {
|
||||
node2: window['app'].nodePreviewImages['2'],
|
||||
node3: window['app'].nodePreviewImages['3']
|
||||
}
|
||||
})
|
||||
|
||||
expect(previewsBefore.node2).toEqual(['fake-preview-url-1'])
|
||||
expect(previewsBefore.node3).toEqual(['fake-preview-url-2'])
|
||||
|
||||
// Mock revokePreviews to track calls and set up event listeners
|
||||
await previewHelper.setupPreviewTracking()
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real execution to trigger progress events that clear previews
|
||||
const testId6 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
// Check if we have progress for nodes 2 and 3
|
||||
const hasNode2Progress = states.some(
|
||||
(state: any) =>
|
||||
state.nodes &&
|
||||
state.nodes['2'] &&
|
||||
state.nodes['2'].state === 'running'
|
||||
)
|
||||
const hasNode3Progress = states.some(
|
||||
(state: any) =>
|
||||
state.nodes &&
|
||||
state.nodes['3'] &&
|
||||
state.nodes['3'].state === 'running'
|
||||
)
|
||||
|
||||
return hasNode2Progress && hasNode3Progress
|
||||
},
|
||||
testId6,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for the event to be processed and previews to be revoked
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const revokedNodes = window['__revokedNodes']
|
||||
const node2PreviewCleared =
|
||||
window['app'].nodePreviewImages['2'] === undefined
|
||||
const node3PreviewCleared =
|
||||
window['app'].nodePreviewImages['3'] === undefined
|
||||
|
||||
return (
|
||||
revokedNodes.includes('2') &&
|
||||
revokedNodes.includes('3') &&
|
||||
node2PreviewCleared &&
|
||||
node3PreviewCleared
|
||||
)
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that revokePreviews was called for both nodes
|
||||
const revokedNodes = await previewHelper.getRevokedNodes()
|
||||
expect(revokedNodes).toContain('2')
|
||||
expect(revokedNodes).toContain('3')
|
||||
|
||||
// Check that previews were cleared
|
||||
const previewsAfter = await comfyPage.page.evaluate(() => {
|
||||
return {
|
||||
node2: window['app'].nodePreviewImages['2'],
|
||||
node3: window['app'].nodePreviewImages['3']
|
||||
}
|
||||
})
|
||||
|
||||
expect(previewsAfter.node2).toBeUndefined()
|
||||
expect(previewsAfter.node3).toBeUndefined()
|
||||
})
|
||||
})
|
||||
168
browser_tests/tests/multiNodeExecutionSimple.spec.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
PreviewTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Multi-node Execution Progress - Simple', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let previewHelper: PreviewTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
previewHelper = new PreviewTestHelper(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Can dispatch and receive progress_state events', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Set up event tracking
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for real progress_state events from backend
|
||||
const testId = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
return latestState.nodes && Object.keys(latestState.nodes).length > 0
|
||||
},
|
||||
testId,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Get the captured states
|
||||
const eventState = await executionHelper.getEventState()
|
||||
const result = eventState.progressStates
|
||||
|
||||
// Should have captured real events
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
const firstState = result[0]
|
||||
expect(firstState).toBeDefined()
|
||||
expect(firstState.prompt_id).toBeDefined()
|
||||
expect(firstState.nodes).toBeDefined()
|
||||
|
||||
// Check that we got real node progress
|
||||
const nodeIds = Object.keys(firstState.nodes)
|
||||
expect(nodeIds.length).toBeGreaterThan(0)
|
||||
|
||||
// Verify node structure
|
||||
for (const nodeId of nodeIds) {
|
||||
const node = firstState.nodes[nodeId]
|
||||
expect(node.state).toBeDefined()
|
||||
expect(node.node_id).toBeDefined()
|
||||
expect(node.display_node_id).toBeDefined()
|
||||
expect(node.prompt_id).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
test('Canvas updates when nodes have progress', async ({ comfyPage, ws }) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Set up progress tracking
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution
|
||||
await comfyPage.queueButton.click()
|
||||
|
||||
// Wait for nodes to have progress from real execution
|
||||
const testId2 = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
// Check if any nodes are running with progress
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running' && node.value > 0
|
||||
)
|
||||
|
||||
return runningNodes.length > 0
|
||||
},
|
||||
testId2,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Wait for progress to be applied to graph nodes
|
||||
await executionHelper.waitForGraphNodeProgress([2, 3, 4])
|
||||
|
||||
// Check that nodes have progress set from real execution
|
||||
const node2Progress = await executionHelper.getGraphNodeProgress(2)
|
||||
const node3Progress = await executionHelper.getGraphNodeProgress(3)
|
||||
const node4Progress = await executionHelper.getGraphNodeProgress(4)
|
||||
|
||||
// At least one node should have progress
|
||||
const hasProgress =
|
||||
(node2Progress !== undefined && node2Progress > 0) ||
|
||||
(node3Progress !== undefined && node3Progress > 0) ||
|
||||
(node4Progress !== undefined && node4Progress > 0)
|
||||
|
||||
expect(hasProgress).toBe(true)
|
||||
})
|
||||
|
||||
test('Preview events include metadata', async ({ comfyPage, ws }) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Track preview events
|
||||
await previewHelper.setupPreviewTracking()
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// For this test, we'll check the event structure by simulating one
|
||||
// since real preview events depend on the workflow actually generating images
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
// Simulate a preview event that would come from backend
|
||||
api.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob: new Blob(['test'], { type: 'image/png' }),
|
||||
nodeId: '10:5:3',
|
||||
displayNodeId: '10',
|
||||
parentNodeId: '10:5',
|
||||
realNodeId: '3',
|
||||
promptId: 'test-prompt-id'
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check captured events
|
||||
const captured = await previewHelper.getPreviewEvents()
|
||||
expect(captured).toHaveLength(1)
|
||||
expect(captured[0]).toEqual({
|
||||
nodeId: '10:5:3',
|
||||
displayNodeId: '10',
|
||||
parentNodeId: '10:5',
|
||||
realNodeId: '3',
|
||||
promptId: 'test-prompt-id'
|
||||
})
|
||||
})
|
||||
})
|
||||
272
browser_tests/tests/previewWithMetadata.spec.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
PreviewTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Preview with Metadata', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let previewHelper: PreviewTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
previewHelper = new PreviewTestHelper(comfyPage.page)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Handles b_preview_with_metadata event correctly', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Clear any existing previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].nodePreviewImages = {}
|
||||
})
|
||||
|
||||
// Set up handler to track preview events and execution
|
||||
await executionHelper.setupEventTracking()
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['__previewHandled'] = false
|
||||
const api = window['app'].api
|
||||
|
||||
// Add handler to track preview events
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
const { displayNodeId, blob } = event.detail
|
||||
// Create URL from the blob in the event
|
||||
const url = URL.createObjectURL(blob)
|
||||
window['app'].nodePreviewImages[displayNodeId] = [url]
|
||||
window['__previewHandled'] = true
|
||||
window['__lastPreviewUrl'] = url
|
||||
})
|
||||
})
|
||||
|
||||
// Start real execution to test event handling in context
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Trigger b_preview_with_metadata event (simulating what backend would send)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const event = new CustomEvent('b_preview_with_metadata', {
|
||||
detail: {
|
||||
blob: new Blob(['test'], { type: 'image/png' }),
|
||||
nodeId: '2',
|
||||
displayNodeId: '2',
|
||||
parentNodeId: '2',
|
||||
realNodeId: '2',
|
||||
promptId: 'test-prompt-id'
|
||||
}
|
||||
})
|
||||
api.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for preview to be handled
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => window['__previewHandled'] === true,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check that preview was set for the correct node
|
||||
const result = await comfyPage.page.evaluate(() => {
|
||||
return {
|
||||
previewImages: window['app'].nodePreviewImages,
|
||||
lastUrl: window['__lastPreviewUrl']
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.previewImages['2']).toBeDefined()
|
||||
expect(result.previewImages['2']).toHaveLength(1)
|
||||
expect(result.previewImages['2'][0]).toBe(result.lastUrl)
|
||||
})
|
||||
|
||||
test('Clears old previews when new preview arrives', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Set up initial preview
|
||||
const initialBlobUrl = await comfyPage.page.evaluate(() => {
|
||||
const blob = new Blob(['initial image'], { type: 'image/png' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window['app'].nodePreviewImages['3'] = [url]
|
||||
return url
|
||||
})
|
||||
|
||||
// Create spy to track URL revocations
|
||||
await previewHelper.setupPreviewTracking()
|
||||
|
||||
// Mock the handler to revoke old previews
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
const { displayNodeId } = event.detail
|
||||
window['app'].revokePreviews(displayNodeId)
|
||||
const newBlob = new Blob(['new image'], { type: 'image/png' })
|
||||
const newUrl = URL.createObjectURL(newBlob)
|
||||
window['app'].nodePreviewImages[displayNodeId] = [newUrl]
|
||||
})
|
||||
})
|
||||
|
||||
// Trigger new preview for same node
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const event = new CustomEvent('b_preview_with_metadata', {
|
||||
detail: {
|
||||
blob: new Blob(['new image'], { type: 'image/png' }),
|
||||
nodeId: '3',
|
||||
displayNodeId: '3',
|
||||
parentNodeId: '3',
|
||||
realNodeId: '3',
|
||||
promptId: 'test-prompt-id'
|
||||
}
|
||||
})
|
||||
api.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check that old URL was revoked
|
||||
const finalRevokedUrls = await previewHelper.getRevokedUrls()
|
||||
expect(finalRevokedUrls).toContain(initialBlobUrl)
|
||||
|
||||
// Check that new preview replaced old one
|
||||
const newPreviewImages = await previewHelper.getNodePreviews('3')
|
||||
|
||||
expect(newPreviewImages).toHaveLength(1)
|
||||
expect(newPreviewImages[0]).not.toBe(initialBlobUrl)
|
||||
})
|
||||
|
||||
test('Associates preview with correct display node in subgraph', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Mock handler that stores metadata
|
||||
await previewHelper.setupPreviewTracking()
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['__previewMetadata'] = {}
|
||||
const api = window['app'].api
|
||||
api.addEventListener('b_preview_with_metadata', (event) => {
|
||||
const { displayNodeId, nodeId, parentNodeId, realNodeId, promptId } =
|
||||
event.detail
|
||||
window['__previewMetadata'][displayNodeId] = {
|
||||
nodeId,
|
||||
displayNodeId,
|
||||
parentNodeId,
|
||||
realNodeId,
|
||||
promptId
|
||||
}
|
||||
// Still create the preview
|
||||
const url = URL.createObjectURL(event.detail.blob)
|
||||
window['app'].nodePreviewImages[displayNodeId] = [url]
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate preview from a subgraph node
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const event = new CustomEvent('b_preview_with_metadata', {
|
||||
detail: {
|
||||
blob: new Blob(['subgraph preview'], { type: 'image/png' }),
|
||||
nodeId: '10:5:3', // Nested execution ID
|
||||
displayNodeId: '10', // Top-level display node
|
||||
parentNodeId: '10:5', // Parent subgraph
|
||||
realNodeId: '3', // Actual node ID within subgraph
|
||||
promptId: 'test-prompt-id'
|
||||
}
|
||||
})
|
||||
api.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check that preview is associated with display node
|
||||
const metadata = await comfyPage.page.evaluate(
|
||||
() => window['__previewMetadata']
|
||||
)
|
||||
expect(metadata['10']).toBeDefined()
|
||||
expect(metadata['10'].nodeId).toBe('10:5:3')
|
||||
expect(metadata['10'].displayNodeId).toBe('10')
|
||||
expect(metadata['10'].parentNodeId).toBe('10:5')
|
||||
expect(metadata['10'].realNodeId).toBe('3')
|
||||
|
||||
// Check that preview exists for display node
|
||||
const previews = await comfyPage.page.evaluate(
|
||||
() => window['app'].nodePreviewImages
|
||||
)
|
||||
expect(previews['10']).toBeDefined()
|
||||
expect(previews['10']).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('Maintains backward compatibility with b_preview event', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
|
||||
|
||||
// Track both events
|
||||
const eventsFired = await comfyPage.page.evaluate(() => {
|
||||
const events: string[] = []
|
||||
const api = window['app'].api
|
||||
|
||||
api.addEventListener('b_preview', () => {
|
||||
events.push('b_preview')
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview_with_metadata', () => {
|
||||
events.push('b_preview_with_metadata')
|
||||
})
|
||||
|
||||
window['__eventsFired'] = events
|
||||
return events
|
||||
})
|
||||
|
||||
// Trigger b_preview_with_metadata
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window['app'].api
|
||||
const blob = new Blob(['test image'], { type: 'image/png' })
|
||||
|
||||
// Simulate the API behavior
|
||||
api.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob,
|
||||
nodeId: '2',
|
||||
displayNodeId: '2',
|
||||
parentNodeId: '2',
|
||||
realNodeId: '2',
|
||||
promptId: 'test-prompt-id'
|
||||
})
|
||||
|
||||
// Also dispatch legacy event as the API would
|
||||
api.dispatchCustomEvent('b_preview', blob)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Check that both events were fired
|
||||
const finalEvents = await comfyPage.page.evaluate(
|
||||
() => window['__eventsFired']
|
||||
)
|
||||
expect(finalEvents).toContain('b_preview_with_metadata')
|
||||
expect(finalEvents).toContain('b_preview')
|
||||
})
|
||||
})
|
||||
82
browser_tests/tests/subgraphBreadcrumb.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Subgraph Breadcrumb Title Sync', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('Breadcrumb updates when subgraph node title is changed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Load a workflow with subgraphs
|
||||
await comfyPage.loadWorkflow('nested-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Get the subgraph node by ID (node 10 is the subgraph)
|
||||
const subgraphNode = await comfyPage.getNodeRefById('10')
|
||||
|
||||
// Get node position and double-click on it to enter the subgraph
|
||||
const nodePos = await subgraphNode.getPosition()
|
||||
const nodeSize = await subgraphNode.getSize()
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y + nodeSize.height / 2 + 10
|
||||
}
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for breadcrumb to appear
|
||||
await comfyPage.page.waitForSelector('.subgraph-breadcrumb', {
|
||||
state: 'visible',
|
||||
timeout: 20000
|
||||
})
|
||||
|
||||
// Get initial breadcrumb text
|
||||
const breadcrumb = comfyPage.page.locator('.subgraph-breadcrumb')
|
||||
const initialBreadcrumbText = await breadcrumb.textContent()
|
||||
|
||||
// Go back to main graph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
// Double-click on the title area of the subgraph node to edit
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y - 10 // Title area is above the node body
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
|
||||
// Wait for title editor to appear
|
||||
await expect(comfyPage.page.locator('.node-title-editor')).toBeVisible()
|
||||
|
||||
// Clear existing text and type new title
|
||||
await comfyPage.page.keyboard.press('Control+a')
|
||||
const newTitle = 'Updated Subgraph Title'
|
||||
await comfyPage.page.keyboard.type(newTitle)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Wait a frame for the update to complete
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Enter the subgraph again
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y + nodeSize.height / 2
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
|
||||
// Wait for breadcrumb
|
||||
await comfyPage.page.waitForSelector('.subgraph-breadcrumb')
|
||||
|
||||
// Check that breadcrumb now shows the new title
|
||||
const updatedBreadcrumbText = await breadcrumb.textContent()
|
||||
expect(updatedBreadcrumbText).toContain(newTitle)
|
||||
expect(updatedBreadcrumbText).not.toBe(initialBreadcrumbText)
|
||||
})
|
||||
})
|
||||
237
browser_tests/tests/subgraphExecutionProgress.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { webSocketFixture } from '../fixtures/ws'
|
||||
import {
|
||||
ExecutionTestHelper,
|
||||
SubgraphTestHelper
|
||||
} from '../helpers/ExecutionTestHelper'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Subgraph Execution Progress', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
test.setTimeout(30000) // Increase timeout for subgraph tests
|
||||
|
||||
let executionHelper: ExecutionTestHelper
|
||||
let subgraphHelper: SubgraphTestHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
executionHelper = new ExecutionTestHelper(comfyPage.page)
|
||||
subgraphHelper = new SubgraphTestHelper(comfyPage.page)
|
||||
// Share the same test ID to access the same window properties
|
||||
subgraphHelper.setTestId(executionHelper.getTestId())
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (executionHelper) {
|
||||
await executionHelper.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('Shows progress for nodes inside subgraphs', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/nested-subgraph-test')
|
||||
|
||||
// Get reference to the subgraph node
|
||||
const subgraphNode = await comfyPage.getNodeRefById(10)
|
||||
expect(subgraphNode).toBeDefined()
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command to avoid click issues
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real progress events from subgraph execution
|
||||
await subgraphHelper.waitForNestedNodeProgress(1)
|
||||
|
||||
// Wait for progress to be applied to the subgraph node
|
||||
await executionHelper.waitForGraphNodeProgress([10])
|
||||
|
||||
// Check that the subgraph node shows aggregated progress
|
||||
const subgraphProgress = await subgraphNode.getProperty('progress')
|
||||
|
||||
// The progress should be aggregated from child nodes
|
||||
expect(subgraphProgress).toBeDefined()
|
||||
expect(subgraphProgress).toBeGreaterThan(0)
|
||||
expect(subgraphProgress).toBeLessThanOrEqual(1)
|
||||
|
||||
// Wait for stroke style to be applied
|
||||
await comfyPage.page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node?.strokeStyles?.['running']) return false
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color === '#0f0'
|
||||
},
|
||||
10,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Check stroke style
|
||||
const strokeStyle = await comfyPage.page.evaluate((nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node || !node.strokeStyles || !node.strokeStyles['running']) {
|
||||
return null
|
||||
}
|
||||
return node.strokeStyles['running'].call(node)
|
||||
}, 10)
|
||||
|
||||
expect(strokeStyle).toEqual({ color: '#0f0' })
|
||||
})
|
||||
|
||||
test('Handles deeply nested subgraph execution', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/nested-subgraph-test')
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command to avoid click issues
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real progress events from deeply nested subgraph execution
|
||||
await subgraphHelper.waitForNestedNodeProgress(2)
|
||||
|
||||
// Wait for progress to be applied to the top-level subgraph node
|
||||
await executionHelper.waitForGraphNodeProgress([10])
|
||||
|
||||
// Check that top-level subgraph shows progress
|
||||
const subgraphNode = await comfyPage.getNodeRefById(10)
|
||||
const progress = await subgraphNode.getProperty('progress')
|
||||
|
||||
expect(progress).toBeDefined()
|
||||
expect(progress).toBeGreaterThan(0)
|
||||
expect(progress).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('Shows running state for parent nodes when child executes', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('execution/nested-subgraph-test')
|
||||
|
||||
// Track which nodes have running stroke style
|
||||
const getRunningNodes = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
const runningNodes: number[] = []
|
||||
const nodes = window['app'].graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
if (
|
||||
node.strokeStyles?.['running'] &&
|
||||
node.strokeStyles['running'].call(node)?.color === '#0f0'
|
||||
) {
|
||||
runningNodes.push(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
return runningNodes
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for any existing execution to complete
|
||||
await comfyPage.page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const nodes = window['app'].graph.nodes
|
||||
for (const node of nodes) {
|
||||
if (
|
||||
node.strokeStyles?.['running'] &&
|
||||
node.strokeStyles['running'].call(node)?.color === '#0f0'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
.catch(() => {
|
||||
// If timeout, continue anyway
|
||||
})
|
||||
|
||||
// Initially no nodes should be running
|
||||
let runningNodes = await getRunningNodes()
|
||||
expect(runningNodes).toHaveLength(0)
|
||||
|
||||
// Set up tracking for progress events
|
||||
await executionHelper.setupEventTracking()
|
||||
|
||||
// Start real execution using command to avoid click issues
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
// Wait for execution to start
|
||||
await executionHelper.waitForExecutionStart()
|
||||
|
||||
// Wait for real nested node execution progress
|
||||
await subgraphHelper.waitForNestedNodeProgress(1)
|
||||
|
||||
// Wait for parent subgraph to show as running
|
||||
await comfyPage.page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node?.strokeStyles?.['running']) return false
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color === '#0f0'
|
||||
},
|
||||
10,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Parent subgraph should show as running
|
||||
runningNodes = await getRunningNodes()
|
||||
expect(runningNodes).toContain(10)
|
||||
|
||||
// Wait for the execution to complete naturally
|
||||
const testId = executionHelper.getTestId()
|
||||
await comfyPage.page.waitForFunction(
|
||||
(testId) => {
|
||||
const states = window[`__progressStates_${testId}`]
|
||||
if (!states || states.length === 0) return false
|
||||
|
||||
// Check if execution is finished (no more running nodes)
|
||||
const latestState = states[states.length - 1]
|
||||
if (!latestState.nodes) return false
|
||||
|
||||
const runningNodes = Object.values(latestState.nodes).filter(
|
||||
(node: any) => node.state === 'running'
|
||||
).length
|
||||
|
||||
return runningNodes === 0
|
||||
},
|
||||
testId,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Add a small delay to ensure UI updates
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Wait for parent subgraph to no longer be running
|
||||
await comfyPage.page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const node = window['app'].graph.getNodeById(nodeId)
|
||||
if (!node?.strokeStyles?.['running']) return true
|
||||
const style = node.strokeStyles['running'].call(node)
|
||||
return style?.color !== '#0f0'
|
||||
},
|
||||
10,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// Parent should no longer be running
|
||||
runningNodes = await getRunningNodes()
|
||||
expect(runningNodes).not.toContain(10)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
72
dangerfile.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { danger, fail } from 'danger'
|
||||
|
||||
// Check if we should run the checks
|
||||
const shouldRunChecks = async () => {
|
||||
const allChangedFiles = [
|
||||
...danger.git.modified_files,
|
||||
...danger.git.created_files
|
||||
]
|
||||
const srcChanges = allChangedFiles.filter((file) => file.startsWith('src/'))
|
||||
|
||||
if (srcChanges.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check total lines changed in src files
|
||||
let totalLinesChanged = 0
|
||||
for (const file of srcChanges) {
|
||||
const diff = await danger.git.diffForFile(file)
|
||||
if (diff) {
|
||||
// Count only lines with actual content (non-empty after trimming whitespace)
|
||||
// This excludes empty lines and lines containing only spaces/tabs
|
||||
const additions =
|
||||
diff.added?.split('\n').filter((line) => line.trim()).length || 0
|
||||
const deletions =
|
||||
diff.removed?.split('\n').filter((line) => line.trim()).length || 0
|
||||
totalLinesChanged += additions + deletions
|
||||
}
|
||||
}
|
||||
|
||||
return totalLinesChanged > 3
|
||||
}
|
||||
|
||||
// Check if browser tests were updated
|
||||
const checkBrowserTestCoverage = () => {
|
||||
const allChangedFiles = [
|
||||
...danger.git.modified_files,
|
||||
...danger.git.created_files
|
||||
]
|
||||
const hasBrowserTestChanges = allChangedFiles.some(
|
||||
(file) => file.startsWith('browser_tests/') && file.endsWith('.ts')
|
||||
)
|
||||
|
||||
if (!hasBrowserTestChanges) {
|
||||
fail(`🧪 **E2E Test Coverage Missing**
|
||||
|
||||
All changes should be covered under E2E testing. Please add or update browser tests.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for screen recording in PR description
|
||||
const checkScreenRecording = () => {
|
||||
const description = danger.github.pr.body || ''
|
||||
const hasRecording =
|
||||
/github\.com\/user-attachments\/assets\/[a-f0-9-]+/i.test(description) ||
|
||||
/youtube\.com\/watch|youtu\.be\//i.test(description)
|
||||
|
||||
if (!hasRecording) {
|
||||
fail(`📹 **Visual Documentation Missing**
|
||||
|
||||
Please add a screen recording or screenshot:
|
||||
- GitHub: Drag & drop media to PR description
|
||||
- YouTube: Add YouTube link`)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the checks only if conditions are met
|
||||
shouldRunChecks().then((shouldRun) => {
|
||||
if (shouldRun) {
|
||||
checkBrowserTestCoverage()
|
||||
checkScreenRecording()
|
||||
}
|
||||
})
|
||||
818
package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.24.0",
|
||||
"version": "1.24.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -45,6 +45,7 @@
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"chalk": "^5.3.0",
|
||||
"danger": "^13.0.4",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
@@ -77,7 +78,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.16.6",
|
||||
"@comfyorg/litegraph": "^0.16.9",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -166,7 +166,7 @@ async function checkNewUnusedKeys() {
|
||||
|
||||
// Report results
|
||||
if (unusedNewKeys.length > 0) {
|
||||
console.log('\n❌ Found unused NEW i18n keys:\n')
|
||||
console.log('\n⚠️ Warning: Found unused NEW i18n keys:\n')
|
||||
|
||||
for (const key of unusedNewKeys.sort()) {
|
||||
console.log(` - ${key}`)
|
||||
@@ -176,9 +176,10 @@ async function checkNewUnusedKeys() {
|
||||
console.log(
|
||||
'\nThese keys were added but are not used anywhere in the codebase.'
|
||||
)
|
||||
console.log('Please either use them or remove them before committing.')
|
||||
console.log('Consider using them or removing them in a future update.')
|
||||
|
||||
process.exit(1)
|
||||
// Changed from process.exit(1) to process.exit(0) for warning only
|
||||
process.exit(0)
|
||||
} else {
|
||||
// Silent success - no output needed
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
@@ -192,22 +191,26 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Update the progress of the executing node
|
||||
// Update the progress of executing nodes
|
||||
watch(
|
||||
() =>
|
||||
[
|
||||
executionStore.executingNodeId,
|
||||
executionStore.executingNodeProgress
|
||||
] satisfies [NodeId | null, number | null],
|
||||
([executingNodeId, executingNodeProgress]) => {
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
if (node.id == executingNodeId) {
|
||||
node.progress = executingNodeProgress ?? undefined
|
||||
[executionStore.nodeLocationProgressStates, canvasStore.canvas] as const,
|
||||
([nodeLocationProgressStates, canvas]) => {
|
||||
if (!canvas?.graph) return
|
||||
for (const node of canvas.graph.nodes) {
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(node.id)
|
||||
const progressState = nodeLocationProgressStates[nodeLocatorId]
|
||||
if (progressState && progressState.state === 'running') {
|
||||
node.progress = progressState.value / progressState.max
|
||||
} else {
|
||||
node.progress = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force canvas redraw to ensure progress updates are visible
|
||||
canvas.graph.setDirtyCanvas(true, false)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Update node slot errors
|
||||
|
||||
@@ -41,7 +41,15 @@ const previousCanvasDraggable = ref(true)
|
||||
|
||||
const onEdit = (newValue: string) => {
|
||||
if (titleEditorStore.titleEditorTarget && newValue.trim() !== '') {
|
||||
titleEditorStore.titleEditorTarget.title = newValue.trim()
|
||||
const trimmedTitle = newValue.trim()
|
||||
titleEditorStore.titleEditorTarget.title = trimmedTitle
|
||||
|
||||
// If this is a subgraph node, sync the runtime subgraph name for breadcrumb reactivity
|
||||
const target = titleEditorStore.titleEditorTarget
|
||||
if (target instanceof LGraphNode && target.isSubgraphNode?.()) {
|
||||
target.subgraph.name = trimmedTitle
|
||||
}
|
||||
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
showInput.value = false
|
||||
|
||||
@@ -20,33 +20,44 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
widget?: object
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let executingNodeId: NodeId | null = null
|
||||
let parentNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
// Get the parent node ID from props if provided
|
||||
// For backward compatibility, fall back to the first executing node
|
||||
parentNodeId = props.nodeId
|
||||
})
|
||||
|
||||
// Watch for either a new node has starting execution or overall execution ending
|
||||
const stopWatching = watch(
|
||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
||||
[() => executionStore.executingNodeIds, () => executionStore.isIdle],
|
||||
() => {
|
||||
if (executionStore.isIdle) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if parent node is no longer in the executing nodes list
|
||||
if (
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
parentNodeId &&
|
||||
!executionStore.executingNodeIds.includes(parentNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
|
||||
// Set parent node ID if not set yet
|
||||
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
|
||||
parentNodeId = executionStore.executingNodeIds[0]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -159,7 +159,7 @@ const toggleExpanded = () => {
|
||||
|
||||
const removeTask = async (task: TaskItemImpl) => {
|
||||
if (task.isRunning) {
|
||||
await api.interrupt()
|
||||
await api.interrupt(task.promptId)
|
||||
}
|
||||
await queueStore.delete(task)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
@@ -36,11 +37,34 @@ export const useBrowserTabTitle = () => {
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
const nodeExecutionTitle = computed(() => {
|
||||
// Check if any nodes are in progress
|
||||
const nodeProgressEntries = Object.entries(
|
||||
executionStore.nodeProgressStates
|
||||
)
|
||||
const runningNodes = nodeProgressEntries.filter(
|
||||
([_, state]) => state.state === 'running'
|
||||
)
|
||||
|
||||
if (runningNodes.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// If multiple nodes are running
|
||||
if (runningNodes.length > 1) {
|
||||
return `${executionText.value}[${runningNodes.length} ${t('g.nodesRunning', 'nodes running')}]`
|
||||
}
|
||||
|
||||
// If only one node is running
|
||||
const [nodeId, state] = runningNodes[0]
|
||||
const progress = Math.round((state.value / state.max) * 100)
|
||||
const nodeType =
|
||||
executionStore.activePrompt?.workflow?.changeTracker?.activeState?.nodes.find(
|
||||
(n) => String(n.id) === nodeId
|
||||
)?.type || 'Node'
|
||||
|
||||
return `${executionText.value}[${progress}%] ${nodeType}`
|
||||
})
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -39,6 +40,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const toastStore = useToastStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
@@ -203,7 +205,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
icon: 'pi pi-stop',
|
||||
label: 'Interrupt',
|
||||
function: async () => {
|
||||
await api.interrupt()
|
||||
await api.interrupt(executionStore.activePromptId)
|
||||
toastStore.add({
|
||||
severity: 'info',
|
||||
summary: t('g.interrupted'),
|
||||
|
||||
@@ -23,6 +23,9 @@ export const useTextPreviewWidget = (
|
||||
name: inputSpec.name,
|
||||
component: TextPreviewWidget,
|
||||
inputSpec,
|
||||
componentProps: {
|
||||
nodeId: node.id
|
||||
},
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string | object) => {
|
||||
|
||||
3
src/config/clientFeatureFlags.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"supports_preview_metadata": true
|
||||
}
|
||||
@@ -290,6 +290,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
options: [
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'zh', text: '中文' },
|
||||
{ value: 'zh-TW', text: '繁體中文' },
|
||||
{ value: 'ru', text: 'Русский' },
|
||||
{ value: 'ja', text: '日本語' },
|
||||
{ value: 'ko', text: '한국어' },
|
||||
@@ -439,6 +440,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Use new menu',
|
||||
type: 'combo',
|
||||
options: ['Disabled', 'Top', 'Bottom'],
|
||||
tooltip:
|
||||
'Menu bar position. On mobile devices, the menu is always shown at the top.',
|
||||
migrateDeprecatedValue: (value: string) => {
|
||||
// Floating is now supported by dragging the docked actionbar off.
|
||||
if (value === 'Floating') {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphNode, type NodeId } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||
import {
|
||||
type ExecutableLGraphNode,
|
||||
type ExecutionId,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
SubgraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import { type NodeId } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
@@ -9,10 +15,13 @@ import {
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { ComfyExtension } from '@/types/comfy'
|
||||
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
|
||||
import { GROUP } from '@/utils/executableGroupNodeDto'
|
||||
import { deserialiseAndCreate, serialise } from '@/utils/vintageClipboard'
|
||||
|
||||
import { api } from '../../scripts/api'
|
||||
@@ -26,8 +35,6 @@ type GroupNodeWorkflowData = {
|
||||
nodes: ComfyNode[]
|
||||
}
|
||||
|
||||
const GROUP = Symbol()
|
||||
|
||||
// v1 Prefix + Separator: workflow/
|
||||
// v2 Prefix + Separator: workflow> (ComfyUI_frontend v1.2.63)
|
||||
const PREFIX = 'workflow'
|
||||
@@ -813,6 +820,7 @@ export class GroupNodeHandler {
|
||||
innerNodeIndex++
|
||||
) {
|
||||
const innerNode = this.innerNodes[innerNodeIndex]
|
||||
innerNode.graph ??= this.node.graph
|
||||
|
||||
for (const w of innerNode.widgets ?? []) {
|
||||
if (w.type === 'converted-widget') {
|
||||
@@ -899,7 +907,20 @@ export class GroupNodeHandler {
|
||||
return link
|
||||
}
|
||||
|
||||
this.node.getInnerNodes = () => {
|
||||
/** @internal Used to flatten the subgraph before execution. Recursive; call with no args. */
|
||||
this.node.getInnerNodes = (
|
||||
computedNodeDtos: Map<ExecutionId, ExecutableLGraphNode>,
|
||||
/** The path of subgraph node IDs. */
|
||||
subgraphNodePath: readonly NodeId[] = [],
|
||||
/** The list of nodes to add to. */
|
||||
nodes: ExecutableLGraphNode[] = [],
|
||||
/** The set of visited nodes. */
|
||||
visited = new Set<LGraphNode>()
|
||||
): ExecutableLGraphNode[] => {
|
||||
if (visited.has(this.node))
|
||||
throw new Error('RecursionError: while flattening subgraph')
|
||||
visited.add(this.node)
|
||||
|
||||
if (!this.innerNodes) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.node.setInnerNodes(
|
||||
@@ -910,6 +931,8 @@ export class GroupNodeHandler {
|
||||
innerNode.configure(n)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
innerNode.id = `${this.node.id}:${i}`
|
||||
// @ts-expect-error fixme ts strict error
|
||||
innerNode.graph = this.node.graph
|
||||
return innerNode
|
||||
})
|
||||
)
|
||||
@@ -917,7 +940,31 @@ export class GroupNodeHandler {
|
||||
|
||||
this.updateInnerWidgets()
|
||||
|
||||
return this.innerNodes
|
||||
const subgraphInstanceIdPath = [...subgraphNodePath, this.node.id]
|
||||
|
||||
// Assertion: Deprecated, does not matter.
|
||||
const subgraphNode = (this.node.graph?.getNodeById(
|
||||
subgraphNodePath.at(-1)
|
||||
) ?? undefined) as SubgraphNode | undefined
|
||||
|
||||
for (const node of this.innerNodes) {
|
||||
node.graph ??= this.node.graph
|
||||
|
||||
// Create minimal DTOs rather than cloning the node
|
||||
const currentId = String(node.id)
|
||||
node.id = currentId.split(':').at(-1)
|
||||
const aVeryRealNode = new ExecutableGroupNodeChildDTO(
|
||||
node,
|
||||
subgraphInstanceIdPath,
|
||||
computedNodeDtos,
|
||||
subgraphNode
|
||||
)
|
||||
node.id = currentId
|
||||
aVeryRealNode.groupNodeHandler = this
|
||||
|
||||
nodes.push(aVeryRealNode)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -1178,9 +1225,10 @@ export class GroupNodeHandler {
|
||||
node.onDrawForeground = function (ctx) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onDrawForeground?.apply?.(this, arguments)
|
||||
const progressState = useExecutionStore().nodeProgressStates[this.id]
|
||||
if (
|
||||
// @ts-expect-error fixme ts strict error
|
||||
+app.runningNodeId === this.id &&
|
||||
progressState &&
|
||||
progressState.state === 'running' &&
|
||||
this.runningInternalNodeId !== null
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -1294,6 +1342,7 @@ export class GroupNodeHandler {
|
||||
this.node.onRemoved = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onRemoved?.apply(this, arguments)
|
||||
// api.removeEventListener('progress_state', progress_state)
|
||||
api.removeEventListener('executing', executing)
|
||||
api.removeEventListener('executed', executed)
|
||||
}
|
||||
@@ -1503,6 +1552,9 @@ export class GroupNodeHandler {
|
||||
|
||||
this.linkOutputs(node, i)
|
||||
app.graph.remove(node)
|
||||
|
||||
// Set internal ID to what is expected after workflow is reloaded
|
||||
node.id = `${this.node.id}:${i}`
|
||||
}
|
||||
|
||||
this.linkInputs()
|
||||
@@ -1608,8 +1660,14 @@ async function convertSelectedNodesToGroupNode() {
|
||||
if (nodes.length === 1) {
|
||||
throw new Error('Please select multiple nodes to convert to group node')
|
||||
}
|
||||
if (nodes.some((n) => GroupNodeHandler.isGroupNode(n))) {
|
||||
throw new Error('Selected nodes contain a group node')
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node instanceof SubgraphNode) {
|
||||
throw new Error('Selected nodes contain a subgraph node')
|
||||
}
|
||||
if (GroupNodeHandler.isGroupNode(node)) {
|
||||
throw new Error('Selected nodes contain a group node')
|
||||
}
|
||||
}
|
||||
return await GroupNodeHandler.fromNodes(nodes)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ import ruCommands from './locales/ru/commands.json'
|
||||
import ru from './locales/ru/main.json'
|
||||
import ruNodes from './locales/ru/nodeDefs.json'
|
||||
import ruSettings from './locales/ru/settings.json'
|
||||
import zhTWCommands from './locales/zh-TW/commands.json'
|
||||
import zhTW from './locales/zh-TW/main.json'
|
||||
import zhTWNodes from './locales/zh-TW/nodeDefs.json'
|
||||
import zhTWSettings from './locales/zh-TW/settings.json'
|
||||
import zhCommands from './locales/zh/commands.json'
|
||||
import zh from './locales/zh/main.json'
|
||||
import zhNodes from './locales/zh/nodeDefs.json'
|
||||
@@ -41,6 +45,7 @@ function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
|
||||
const messages = {
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings),
|
||||
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
|
||||
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
|
||||
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
|
||||
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
|
||||
ko: buildLocale(ko, koNodes, koCommands, koSettings),
|
||||
|
||||
172
src/locales/CONTRIBUTING.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Contributing Translations to ComfyUI
|
||||
|
||||
## Quick Start for New Languages
|
||||
|
||||
1. **Let us know** - Open an issue or reach out on Discord to request a new language
|
||||
2. **Get technical setup help** - We'll help configure the initial files or you can follow the technical process below
|
||||
3. **Automatic translation** - Our CI system will generate translations using OpenAI when you create a PR
|
||||
4. **Review and refine** - You can improve the auto-generated translations and become a maintainer for that language
|
||||
|
||||
## Technical Process (Confirmed Working)
|
||||
|
||||
### Prerequisites
|
||||
- Node.js installed
|
||||
- Git/GitHub knowledge
|
||||
- OpenAI API key (optional - CI will handle translations)
|
||||
|
||||
### Step 1: Update Configuration Files
|
||||
|
||||
**Time required: ~10 minutes**
|
||||
|
||||
#### 1.1 Update `.i18nrc.cjs`
|
||||
Add your language code to the `outputLocales` array:
|
||||
|
||||
```javascript
|
||||
module.exports = defineConfig({
|
||||
// ... existing config
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'], // Add your language here
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
|
||||
`
|
||||
});
|
||||
```
|
||||
|
||||
#### 1.2 Update `src/constants/coreSettings.ts`
|
||||
Add your language to the dropdown options:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Language',
|
||||
type: 'combo',
|
||||
options: [
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'zh', text: '中文' },
|
||||
{ value: 'zh-TW', text: '繁體中文 (台灣)' }, // Add your language here
|
||||
{ value: 'ru', text: 'Русский' },
|
||||
{ value: 'ja', text: '日本語' },
|
||||
{ value: 'ko', text: '한국어' },
|
||||
{ value: 'fr', text: 'Français' },
|
||||
{ value: 'es', text: 'Español' }
|
||||
],
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
```
|
||||
|
||||
#### 1.3 Update `src/i18n.ts`
|
||||
Add imports for your new language files:
|
||||
|
||||
```typescript
|
||||
// Add these imports (replace zh-TW with your language code)
|
||||
import zhTWCommands from './locales/zh-TW/commands.json'
|
||||
import zhTW from './locales/zh-TW/main.json'
|
||||
import zhTWNodes from './locales/zh-TW/nodeDefs.json'
|
||||
import zhTWSettings from './locales/zh-TW/settings.json'
|
||||
|
||||
// Add to the messages object
|
||||
const messages = {
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings),
|
||||
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
|
||||
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings), // Add this line
|
||||
// ... other languages
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Generate Translation Files
|
||||
|
||||
#### Option A: Local Generation (Optional)
|
||||
```bash
|
||||
# Only if you have OpenAI API key configured
|
||||
npm run locale
|
||||
```
|
||||
|
||||
#### Option B: Let CI Handle It (Recommended)
|
||||
- Create your PR with the configuration changes above
|
||||
- Our GitHub CI will automatically generate translation files
|
||||
- Empty JSON files are fine - they'll be populated by the workflow
|
||||
|
||||
### Step 3: Test Your Changes
|
||||
|
||||
```bash
|
||||
npm run typecheck # Check for TypeScript errors
|
||||
npm run dev # Start development server
|
||||
```
|
||||
|
||||
**Testing checklist:**
|
||||
- [ ] Language appears in ComfyUI Settings > Locale dropdown
|
||||
- [ ] Can select the new language without errors
|
||||
- [ ] Partial translations display correctly
|
||||
- [ ] UI falls back to English for untranslated strings
|
||||
- [ ] No console errors when switching languages
|
||||
|
||||
### Step 4: Submit PR
|
||||
|
||||
1. **Create PR** with your configuration changes
|
||||
2. **CI will run** and automatically populate translation files
|
||||
3. **Request review** from language maintainers: @Yorha4D @KarryCharon @DorotaLuna @shinshin86
|
||||
4. **Get added to CODEOWNERS** as a reviewer for your language
|
||||
|
||||
## What Happens in CI
|
||||
|
||||
Our automated translation workflow:
|
||||
1. **Collects strings**: Scans the UI for translatable text
|
||||
2. **Updates English files**: Ensures all strings are captured
|
||||
3. **Generates translations**: Uses OpenAI API to translate to all configured languages
|
||||
4. **Commits back**: Automatically updates your PR with complete translations
|
||||
|
||||
## File Structure
|
||||
|
||||
Each language has 4 translation files:
|
||||
- `main.json` - Main UI text (~2000+ entries)
|
||||
- `commands.json` - Command descriptions (~200+ entries)
|
||||
- `settings.json` - Settings panel (~400+ entries)
|
||||
- `nodeDefs.json` - Node definitions (~varies based on installed nodes)
|
||||
|
||||
## Translation Quality
|
||||
|
||||
- **Auto-translations are high quality** but may need refinement
|
||||
- **Technical terms** are preserved (flux, photomaker, clip, vae, etc.)
|
||||
- **Context-aware** translations based on UI usage
|
||||
- **Native speaker review** is encouraged for quality improvements
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: TypeScript errors on imports
|
||||
**Solution**: Ensure your language code matches exactly in all three files
|
||||
|
||||
### Issue: Empty translation files
|
||||
**Solution**: This is normal - CI will populate them when you create a PR
|
||||
|
||||
### Issue: Language not appearing in dropdown
|
||||
**Solution**: Check that the language code in `coreSettings.ts` matches your other files exactly
|
||||
|
||||
### Issue: Rate limits during local translation
|
||||
**Solution**: This is expected - let CI handle the translation generation
|
||||
|
||||
## Regional Variants
|
||||
|
||||
For regional variants (like zh-TW for Taiwan), use:
|
||||
- **Language-region codes**: `zh-TW`, `pt-BR`, `en-US`
|
||||
- **Specific terminology**: Add region-specific context to the reference string
|
||||
- **Native display names**: Use the local language name in the dropdown
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Tag translation maintainers**: @Yorha4D @KarryCharon @DorotaLuna @shinshin86
|
||||
- **Check existing language PRs** for examples
|
||||
- **Open an issue** describing your language addition request
|
||||
- **Reference this tested process** - we've confirmed it works!
|
||||
|
||||
## Becoming a Language Maintainer
|
||||
|
||||
After your language is added:
|
||||
1. **Get added to CODEOWNERS** for your language files
|
||||
2. **Review future PRs** affecting your language
|
||||
3. **Coordinate with other native speakers** for quality improvements
|
||||
4. **Help maintain translations** as the UI evolves
|
||||
|
||||
---
|
||||
|
||||
*This process was tested and confirmed working with Traditional Chinese (Taiwan) addition.*
|
||||
@@ -14,68 +14,17 @@ Our project supports multiple languages using `vue-i18n`. This allows users arou
|
||||
|
||||
## How to Add a New Language
|
||||
|
||||
We welcome the addition of new languages. You can add a new language by following these steps:
|
||||
Want to add a new language to ComfyUI? See our detailed [Contributing Guide](./CONTRIBUTING.md) with step-by-step instructions and confirmed working process.
|
||||
|
||||
### 1\. Generate language files
|
||||
### Quick Start
|
||||
1. Open an issue or reach out on Discord to request a new language
|
||||
2. Follow the [technical process](./CONTRIBUTING.md#technical-process-confirmed-working) or ask for help
|
||||
3. Our CI will automatically generate translations using OpenAI
|
||||
4. Become a maintainer for your language
|
||||
|
||||
We use [lobe-i18n](https://github.com/lobehub/lobe-cli-toolbox/blob/master/packages/lobe-i18n/README.md) as our translation tool, which integrates with LLM for efficient localization.
|
||||
|
||||
Update the configuration file to include the new language(s) you wish to add:
|
||||
|
||||
```javascript
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
entry: 'src/locales/en.json', // Base language file
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'], // Add the new language(s) here
|
||||
});
|
||||
```
|
||||
|
||||
Set your OpenAI API Key by running the following command:
|
||||
|
||||
```sh
|
||||
npx lobe-i18n --option
|
||||
```
|
||||
|
||||
Once configured, generate the translation files with:
|
||||
|
||||
```sh
|
||||
npx lobe-i18n locale
|
||||
```
|
||||
|
||||
This will create the language files for the specified languages in the configuration.
|
||||
|
||||
### 2\. Update i18n Configuration
|
||||
|
||||
Import the newly generated locale file(s) in the `src/i18n.ts` file to include them in the application's i18n setup.
|
||||
|
||||
### 3\. Enable Selection of the New Language
|
||||
|
||||
Add the newly added language to the following item in `src/constants/coreSettings.ts`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
// Add the new language(s) here
|
||||
options: [
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'zh', text: '中文' },
|
||||
{ value: 'ru', text: 'Русский' },
|
||||
{ value: 'ja', text: '日本語' },
|
||||
{ value: 'ko', text: '한국어' },
|
||||
{ value: 'fr', text: 'Français' },
|
||||
{ value: 'es', text: 'Español' }
|
||||
],
|
||||
defaultValue: navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
```
|
||||
|
||||
This will make the new language selectable in the application's settings.
|
||||
|
||||
### 4\. Test the Translations
|
||||
|
||||
Start the development server, switch to the new language, and verify the translations. You can switch languages by opening the ComfyUI Settings and selecting from the `ComfyUI > Locale` dropdown box.
|
||||
### File Structure
|
||||
Each language has 4 translation files in `src/locales/[language-code]/`:
|
||||
- `main.json` - Main UI text
|
||||
- `commands.json` - Command descriptions
|
||||
- `settings.json` - Settings panel
|
||||
- `nodeDefs.json` - Node definitions
|
||||
|
||||
@@ -133,7 +133,8 @@
|
||||
"copyURL": "Copy URL",
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on"
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"nodesRunning": "nodes running"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -547,182 +548,194 @@
|
||||
"Audio": "Audio",
|
||||
"Image API": "Image API",
|
||||
"Video API": "Video API",
|
||||
"LLM API": "LLM API",
|
||||
"All": "All Templates"
|
||||
},
|
||||
"templateDescription": {
|
||||
"Basics": {
|
||||
"default": "Generate images from text descriptions.",
|
||||
"default": "Generate images from text prompts.",
|
||||
"image2image": "Transform existing images using text prompts.",
|
||||
"lora": "Apply LoRA models for specialized styles or subjects.",
|
||||
"lora": "Generate images with LoRA models for specialized styles or subjects.",
|
||||
"lora_multiple": "Generate images by combining multiple LoRA models.",
|
||||
"inpaint_example": "Edit specific parts of images seamlessly.",
|
||||
"inpain_model_outpainting": "Extend images beyond their original boundaries.",
|
||||
"embedding_example": "Use textual inversion for consistent styles.",
|
||||
"gligen_textbox_example": "Specify the location and size of objects.",
|
||||
"lora_multiple": "Combine multiple LoRA models for unique results."
|
||||
"inpaint_model_outpainting": "Extend images beyond their original boundaries.",
|
||||
"embedding_example": "Generate images using textual inversion for consistent styles.",
|
||||
"gligen_textbox_example": "Generate images with precise object placement using text boxes."
|
||||
},
|
||||
"Flux": {
|
||||
"flux_dev_checkpoint_example": "Create images using Flux development models.",
|
||||
"flux_schnell": "Generate images quickly with Flux Schnell.",
|
||||
"flux_fill_inpaint_example": "Fill in missing parts of images.",
|
||||
"flux_fill_outpaint_example": "Extend images using Flux outpainting.",
|
||||
"flux_canny_model_example": "Generate images from edge detection.",
|
||||
"flux_depth_lora_example": "Create images with depth-aware LoRA.",
|
||||
"flux_redux_model_example": "Transfer style from a reference image to guide image generation with Flux."
|
||||
"flux_kontext_dev_basic": "Edit image using Flux Kontext with full node visibility, perfect for learning the workflow.",
|
||||
"flux_kontext_dev_grouped": "Streamlined version of Flux Kontext with grouped nodes for cleaner workspace.",
|
||||
"flux_dev_checkpoint_example": "Generate images using Flux Dev fp8 quantized version. Suitable for devices with limited VRAM, requires only one model file, but image quality is slightly lower than the full version.",
|
||||
"flux_schnell": "Quickly generate images with Flux Schnell fp8 quantized version. Ideal for low-end hardware, requires only 4 steps to generate images.",
|
||||
"flux_dev_full_text_to_image": "Generate high-quality images with Flux Dev full version. Requires larger VRAM and multiple model files, but provides the best prompt following capability and image quality.",
|
||||
"flux_schnell_full_text_to_image": "Generate images quickly with Flux Schnell full version. Uses Apache2.0 license, requires only 4 steps to generate images while maintaining good image quality.",
|
||||
"flux_fill_inpaint_example": "Fill missing parts of images using Flux inpainting.",
|
||||
"flux_fill_outpaint_example": "Extend images beyond boundaries using Flux outpainting.",
|
||||
"flux_canny_model_example": "Generate images guided by edge detection using Flux Canny.",
|
||||
"flux_depth_lora_example": "Generate images guided by depth information using Flux LoRA.",
|
||||
"flux_redux_model_example": "Generate images by transferring style from reference images using Flux Redux."
|
||||
},
|
||||
"Image": {
|
||||
"hidream_i1_dev": "Generate images with HiDream I1 Dev.",
|
||||
"hidream_i1_fast": "Generate images quickly with HiDream I1.",
|
||||
"hidream_i1_full": "Generate images with HiDream I1.",
|
||||
"hidream_e1_full": "Edit images with HiDream E1.",
|
||||
"sd3_5_simple_example": "Generate images with SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Use edge detection to guide image generation with SD 3.5.",
|
||||
"sd3_5_large_depth": "Create depth-aware images with SD 3.5.",
|
||||
"sd3_5_large_blur": "Generate images from blurred reference images with SD 3.5.",
|
||||
"sdxl_simple_example": "Create high-quality images with SDXL.",
|
||||
"sdxl_refiner_prompt_example": "Enhance SDXL outputs with refiners.",
|
||||
"sdxl_revision_text_prompts": "Transfer concepts from reference images to guide image generation with SDXL.",
|
||||
"sdxl_revision_zero_positive": "Add text prompts alongside reference images to guide image generation with SDXL.",
|
||||
"sdxlturbo_example": "Generate images in a single step with SDXL Turbo."
|
||||
"image_omnigen2_t2i": "Generate high-quality images from text prompts using OmniGen2's unified 7B multimodal model with dual-path architecture.",
|
||||
"image_omnigen2_image_edit": "Edit images with natural language instructions using OmniGen2's advanced image editing capabilities and text rendering support.",
|
||||
"image_cosmos_predict2_2B_t2i": "Generate images with Cosmos-Predict2 2B T2I, delivering physically accurate, high-fidelity, and detail-rich image generation.",
|
||||
"image_chroma_text_to_image": "Chroma is modified from flux and has some changes in the architecture.",
|
||||
"hidream_i1_dev": "Generate images with HiDream I1 Dev - Balanced version with 28 inference steps, suitable for medium-range hardware.",
|
||||
"hidream_i1_fast": "Generate images quickly with HiDream I1 Fast - Lightweight version with 16 inference steps, ideal for rapid previews on lower-end hardware.",
|
||||
"hidream_i1_full": "Generate images with HiDream I1 Full - Complete version with 50 inference steps for highest quality output.",
|
||||
"hidream_e1_full": "Edit images with HiDream E1 - Professional natural language image editing model.",
|
||||
"sd3_5_simple_example": "Generate images using SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Generate images guided by edge detection using SD 3.5 Canny ControlNet.",
|
||||
"sd3_5_large_depth": "Generate images guided by depth information using SD 3.5.",
|
||||
"sd3_5_large_blur": "Generate images guided by blurred reference images using SD 3.5.",
|
||||
"sdxl_simple_example": "Generate high-quality images using SDXL.",
|
||||
"sdxl_refiner_prompt_example": "Enhance SDXL images using refiner models.",
|
||||
"sdxl_revision_text_prompts": "Generate images by transferring concepts from reference images using SDXL Revision.",
|
||||
"sdxl_revision_zero_positive": "Generate images using both text prompts and reference images with SDXL Revision.",
|
||||
"sdxlturbo_example": "Generate images in a single step using SDXL Turbo.",
|
||||
"image_lotus_depth_v1_1": "Run Lotus Depth in ComfyUI for zero-shot, efficient monocular depth estimation with high detail retention."
|
||||
},
|
||||
"Video": {
|
||||
"text_to_video_wan": "Quickly Generate videos from text descriptions.",
|
||||
"image_to_video_wan": "Quickly Generate videos from images.",
|
||||
"wan2_1_fun_inp": "Create videos from start and end frames.",
|
||||
"wan2_1_fun_control": "Guide video generation with pose, depth, edge controls and more.",
|
||||
"wan2_1_flf2v_720_f16": "Generate video through controlling the first and last frames.",
|
||||
"ltxv_text_to_video": "Generate videos from text descriptions.",
|
||||
"ltxv_image_to_video": "Convert still images into videos.",
|
||||
"mochi_text_to_video_example": "Create videos with Mochi model.",
|
||||
"hunyuan_video_text_to_video": "Generate videos using Hunyuan model.",
|
||||
"image_to_video": "Transform images into animated videos.",
|
||||
"txt_to_image_to_video": "Generate images from text and then convert them into videos."
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Generate videos with Cosmos-Predict2 2B Video2World, generating physically accurate, high-fidelity, and consistent video simulations.",
|
||||
"video_wan_vace_14B_t2v": "Transform text descriptions into high-quality videos. Supports both 480p and 720p with VACE-14B model.",
|
||||
"video_wan_vace_14B_ref2v": "Create videos that match the style and content of a reference image. Perfect for style-consistent video generation.",
|
||||
"video_wan_vace_14B_v2v": "Generate videos by controlling input videos and reference images using Wan VACE.",
|
||||
"video_wan_vace_outpainting": "Generate extended videos by expanding video size using Wan VACE outpainting.",
|
||||
"video_wan_vace_flf2v": "Generate smooth video transitions by defining start and end frames. Supports custom keyframe sequences.",
|
||||
"video_wan_vace_inpainting": "Edit specific regions in videos while preserving surrounding content. Great for object removal or replacement.",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Generate dynamic videos with cinematic camera movements using Wan 2.1 Fun Camera 1.3B model.",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Generate high-quality videos with advanced camera control using the full 14B model",
|
||||
"text_to_video_wan": "Generate videos from text prompts using Wan 2.1.",
|
||||
"image_to_video_wan": "Generate videos from images using Wan 2.1.",
|
||||
"wan2_1_fun_inp": "Generate videos from start and end frames using Wan 2.1 inpainting.",
|
||||
"wan2_1_fun_control": "Generate videos guided by pose, depth, and edge controls using Wan 2.1 ControlNet.",
|
||||
"wan2_1_flf2v_720_f16": "Generate videos by controlling first and last frames using Wan 2.1 FLF2V.",
|
||||
"ltxv_text_to_video": "Generate videos from text prompts.",
|
||||
"ltxv_image_to_video": "Generate videos from still images.",
|
||||
"mochi_text_to_video_example": "Generate videos from text prompts using Mochi model.",
|
||||
"hunyuan_video_text_to_video": "Generate videos from text prompts using Hunyuan model.",
|
||||
"image_to_video": "Generate videos from still images.",
|
||||
"txt_to_image_to_video": "Generate videos by first creating images from text prompts."
|
||||
},
|
||||
"Image API": {
|
||||
"api_openai_image_1_t2i": "Use GPT Image 1 API to generate images from text descriptions.",
|
||||
"api_openai_image_1_i2i": "Use GPT Image 1 API to generate images from images.",
|
||||
"api_openai_image_1_inpaint": "Use GPT Image 1 API to inpaint images.",
|
||||
"api_openai_image_1_multi_inputs": "Use GPT Image 1 API with multiple inputs to generate images.",
|
||||
"api-openai-dall-e-2-t2i": "Use Dall-E 2 API to generate images from text descriptions.",
|
||||
"api-openai-dall-e-2-inpaint": "Use Dall-E 2 API to inpaint images.",
|
||||
"api-openai-dall-e-3-t2i": "Use Dall-E 3 API to generate images from text descriptions.",
|
||||
"api_bfl_flux_pro_t2i": "Create images with FLUX.1 [pro]'s excellent prompt following, visual quality, image detail and output diversity.",
|
||||
"api_stability_sd3_t2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
|
||||
"api_ideogram_v3_t2i": "Generate images with high-quality image-prompt alignment, photorealism, and text rendering. Create professional-quality logos, promotional posters, landing page concepts, product photography, and more. Effortlessly craft sophisticated spatial compositions with intricate backgrounds, precise and nuanced lighting and colors, and lifelike environmental detail.",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "Input multiple images and edit them with Flux.1 Kontext.",
|
||||
"api_bfl_flux_1_kontext_pro_image": "Edit images with Flux.1 Kontext pro image.",
|
||||
"api_bfl_flux_1_kontext_max_image": "Edit images with Flux.1 Kontext max image.",
|
||||
"api_bfl_flux_pro_t2i": "Generate images with excellent prompt following and visual quality using FLUX.1 Pro.",
|
||||
"api_luma_photon_i2i": "Guide image generation using a combination of images and prompt.",
|
||||
"api_luma_photon_style_ref": "Apply and blend style references with exact control. Luma Photon captures the essence of each reference image, letting you combine distinct visual elements while maintaining professional quality.",
|
||||
"api_recraft_image_gen_with_color_control": "Create a custom palette to reuse for multiple images or hand-pick colors for each photo. Match your brand's color palette and craft visuals that are distinctly yours.",
|
||||
"api_luma_photon_style_ref": "Generate images by blending style references with precise control using Luma Photon.",
|
||||
"api_recraft_image_gen_with_color_control": "Generate images with custom color palettes and brand-specific visuals using Recraft.",
|
||||
"api_recraft_image_gen_with_style_control": "Control style with visual examples, align positioning, and fine-tune objects. Store and share styles for perfect brand consistency.",
|
||||
"api_recraft_vector_gen": "Go from a text prompt to vector image with Recraft's AI vector generator. Produce the best-quality vector art for logos, posters, icon sets, ads, banners and mockups. Perfect your designs with sharp, high-quality SVG files. Create branded vector illustrations for your app or website in seconds."
|
||||
"api_recraft_vector_gen": "Generate high-quality vector images from text prompts using Recraft's AI vector generator.",
|
||||
"api_runway_text_to_image": "Generate high-quality images from text prompts using Runway's AI model.",
|
||||
"api_runway_reference_to_image": "Generate new images based on reference styles and compositions with Runway's AI.",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
|
||||
"api_stability_ai_i2i": "Transform images with high-quality generation using Stability AI, perfect for professional editing and style transfer.",
|
||||
"api_stability_ai_sd3_5_t2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
|
||||
"api_stability_ai_sd3_5_i2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
|
||||
"api_ideogram_v3_t2i": "Generate professional-quality images with excellent prompt alignment, photorealism, and text rendering using Ideogram V3.",
|
||||
"api_openai_image_1_t2i": "Generate images from text prompts using OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_i2i": "Generate images from input images using OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_inpaint": "Edit images using inpainting with OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_multi_inputs": "Generate images from multiple inputs using OpenAI GPT Image 1 API.",
|
||||
"api_openai_dall_e_2_t2i": "Generate images from text prompts using OpenAI Dall-E 2 API.",
|
||||
"api_openai_dall_e_2_inpaint": "Edit images using inpainting with OpenAI Dall-E 2 API.",
|
||||
"api_openai_dall_e_3_t2i": "Generate images from text prompts using OpenAI Dall-E 3 API."
|
||||
},
|
||||
"Video API": {
|
||||
"api_moonvalley_text_to_video": "Generate cinematic, 1080p videos from text prompts through a model trained exclusively on licensed data.",
|
||||
"api_moonvalley_image_to_video": "Generate cinematic, 1080p videos with an image through a model trained exclusively on licensed data.",
|
||||
"api_kling_i2v": "Generate videos with excellent prompt adherence for actions, expressions, and camera movements using Kling.",
|
||||
"api_kling_effects": "Generate dynamic videos by applying visual effects to images using Kling.",
|
||||
"api_kling_flf": "Generate videos through controlling the first and last frames.",
|
||||
"api_luma_i2v": "Take static images and instantly create magical high quality animations.",
|
||||
"api_kling_i2v": "Create videos with great prompt adherence for actions, expressions, and camera movements. Now supporting complex prompts with sequential actions for you to be the director of your scene.",
|
||||
"api_veo2_i2v": "Use Google Veo2 API to generate videos from images.",
|
||||
"api_hailuo_minimax_i2v": "Create refined videos from images and text, including CGI integration and trendy photo effects like viral AI hugging. Choose from a variety of video styles and themes to match your creative vision.",
|
||||
"api_pika_scene": "Use multiple images as ingredients and generate videos that incorporate all of them.",
|
||||
"api_pixverse_template_i2v": "Transforms static images into dynamic videos with motion and effects.",
|
||||
"api_pixverse_t2v": "Generate videos with accurate prompt interpretation and stunning video dynamics."
|
||||
"api_luma_t2v": "High-quality videos can be generated using simple prompts.",
|
||||
"api_hailuo_minimax_t2v": "Generate high-quality videos directly from text prompts. Explore MiniMax's advanced AI capabilities to create diverse visual narratives with professional CGI effects and stylistic elements to bring your descriptions to life.",
|
||||
"api_hailuo_minimax_i2v": "Generate refined videos from images and text with CGI integration using MiniMax.",
|
||||
"api_pixverse_i2v": "Generate dynamic videos from static images with motion and effects using PixVerse.",
|
||||
"api_pixverse_template_i2v": "Generate dynamic videos from static images with motion and effects using PixVerse.",
|
||||
"api_pixverse_t2v": "Generate videos with accurate prompt interpretation and stunning video dynamics.",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Generate cinematic videos from static images using Runway Gen3a Turbo.",
|
||||
"api_runway_gen4_turo_image_to_video": "Generate dynamic videos from images using Runway Gen4 Turbo.",
|
||||
"api_runway_first_last_frame": "Generate smooth video transitions between two keyframes with Runway's precision.",
|
||||
"api_pika_i2v": "Generate smooth animated videos from single static images using Pika AI.",
|
||||
"api_pika_scene": "Generate videos that incorporate multiple input images using Pika Scenes.",
|
||||
"api_veo2_i2v": "Generate videos from images using Google Veo2 API."
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Generate detailed 3D models from single photos using Rodin AI.",
|
||||
"api_rodin_multiview_to_model": "Sculpt comprehensive 3D models using Rodin's multi-angle reconstruction.",
|
||||
"api_tripo_text_to_model": "Craft 3D objects from descriptions with Tripo's text-driven modeling.",
|
||||
"api_tripo_image_to_model": "Generate professional 3D assets from 2D images using Tripo engine.",
|
||||
"api_tripo_multiview_to_model": "Build 3D models from multiple angles with Tripo's advanced scanner."
|
||||
},
|
||||
"LLM API": {
|
||||
"api_openai_chat": "Engage with OpenAI's advanced language models for intelligent conversations.",
|
||||
"api_google_gemini": "Experience Google's multimodal AI with Gemini's reasoning capabilities."
|
||||
},
|
||||
"Upscaling": {
|
||||
"hiresfix_latent_workflow": "Enhance image quality in latent space.",
|
||||
"esrgan_example": "Use upscale models to enhance image quality.",
|
||||
"hiresfix_esrgan_workflow": "Use upscale models during intermediate steps.",
|
||||
"latent_upscale_different_prompt_model": "Upscale and change prompt across passes."
|
||||
"hiresfix_latent_workflow": "Upscale images by enhancing quality in latent space.",
|
||||
"esrgan_example": "Upscale images using ESRGAN models to enhance quality.",
|
||||
"hiresfix_esrgan_workflow": "Upscale images using ESRGAN models during intermediate generation steps.",
|
||||
"latent_upscale_different_prompt_model": "Upscale images while changing prompts across generation passes."
|
||||
},
|
||||
"ControlNet": {
|
||||
"controlnet_example": "Control image generation with reference images.",
|
||||
"2_pass_pose_worship": "Generate images from pose references.",
|
||||
"depth_controlnet": "Create images with depth-aware generation.",
|
||||
"depth_t2i_adapter": "Quickly generate depth-aware images with a T2I adapter.",
|
||||
"mixing_controlnets": "Combine multiple ControlNet models together."
|
||||
"controlnet_example": "Generate images guided by scribble reference images using ControlNet.",
|
||||
"2_pass_pose_worship": "Generate images guided by pose references using ControlNet.",
|
||||
"depth_controlnet": "Generate images guided by depth information using ControlNet.",
|
||||
"depth_t2i_adapter": "Generate images guided by depth information using T2I adapter.",
|
||||
"mixing_controlnets": "Generate images by combining multiple ControlNet models."
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Control image composition with areas.",
|
||||
"area_composition_reversed": "Reverse area composition workflow.",
|
||||
"area_composition_square_area_for_subject": "Create consistent subject placement."
|
||||
"area_composition": "Generate images by controlling composition with defined areas.",
|
||||
"area_composition_square_area_for_subject": "Generate images with consistent subject placement using area composition."
|
||||
},
|
||||
"3D": {
|
||||
"hunyuan3d-non-multiview-train": "Use Hunyuan3D 2.0 to generate models from a single view.",
|
||||
"hunyuan-3d-multiview-elf": " Use Hunyuan3D 2mv to generate models from multiple views.",
|
||||
"hunyuan-3d-turbo": "Use Hunyuan3D 2mv turbo to generate models from multiple views.",
|
||||
"stable_zero123_example": "Generate 3D views from single images."
|
||||
"3d_hunyuan3d_image_to_model": "Generate 3D models from single images using Hunyuan3D 2.0.",
|
||||
"3d_hunyuan3d_multiview_to_model": "Generate 3D models from multiple views using Hunyuan3D 2.0 MV.",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Generate 3D models from multiple views using Hunyuan3D 2.0 MV Turbo.",
|
||||
"stable_zero123_example": "Generate 3D views from single images using Stable Zero123."
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Generate audio from text descriptions."
|
||||
"audio_stable_audio_example": "Generate audio from text prompts using Stable Audio.",
|
||||
"audio_ace_step_1_t2a_instrumentals": "Generate instrumental music from text prompts using ACE-Step v1.",
|
||||
"audio_ace_step_1_t2a_song": "Generate songs with vocals from text prompts using ACE-Step v1, supporting multilingual and style customization.",
|
||||
"audio_ace_step_1_m2m_editing": "Edit existing songs to change style and lyrics using ACE-Step v1 M2M."
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"Flux": {
|
||||
"flux_dev_checkpoint_example": "Flux Dev",
|
||||
"flux_schnell": "Flux Schnell",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_canny_model_example": "Flux Canny Model",
|
||||
"flux_redux_model_example": "Flux Redux Model",
|
||||
"flux_depth_lora_example": "Flux Depth Lora"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "Image Generation",
|
||||
"image2image": "Image to Image",
|
||||
"embedding_example": "Embedding",
|
||||
"gligen_textbox_example": "Gligen Textbox",
|
||||
"lora": "Lora",
|
||||
"lora_multiple": "Lora Multiple",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "LoRA Multiple",
|
||||
"inpaint_example": "Inpaint",
|
||||
"inpain_model_outpainting": "Outpaint"
|
||||
"inpaint_model_outpainting": "Outpaint",
|
||||
"embedding_example": "Embedding",
|
||||
"gligen_textbox_example": "Gligen Textbox"
|
||||
},
|
||||
"ControlNet": {
|
||||
"controlnet_example": "Scribble ControlNet",
|
||||
"2_pass_pose_worship": "Pose ControlNet 2 Pass",
|
||||
"depth_controlnet": "Depth ControlNet",
|
||||
"depth_t2i_adapter": "Depth T2I Adapter",
|
||||
"mixing_controlnets": "Mixing ControlNets"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "ESRGAN",
|
||||
"hiresfix_latent_workflow": "Upscale",
|
||||
"hiresfix_esrgan_workflow": "HiresFix ESRGAN Workflow",
|
||||
"latent_upscale_different_prompt_model": "Latent Upscale Different Prompt Model"
|
||||
},
|
||||
"Video": {
|
||||
"text_to_video_wan": "Wan 2.1 Text to Video",
|
||||
"image_to_video_wan": "Wan 2.1 Image to Video",
|
||||
"image_to_video": "SVD Image to Video",
|
||||
"txt_to_image_to_video": "SVD Text to Image to Video",
|
||||
"ltxv_image_to_video": "LTXV Image to Video",
|
||||
"ltxv_text_to_video": "LTXV Text to Video",
|
||||
"mochi_text_to_video_example": "Mochi Text to Video",
|
||||
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video",
|
||||
"wan2_1_fun_inp": "Wan 2.1 Inpainting",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet"
|
||||
},
|
||||
"Image API": {
|
||||
"api_openai_image_1_t2i": "OpenAI Image-1 Text to Image",
|
||||
"api_openai_image_1_i2i": "OpenAI Image-1 Image to Image",
|
||||
"api_openai_image_1_inpaint": "OpenAI Image-1 Inpaint",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI Image-1 Multi Inputs",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 Text to Image",
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 Inpaint",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 Text to Image",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra Text to Image",
|
||||
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra Text to Image",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 Text to Image",
|
||||
"api_luma_photon_i2i": "Luma Photon Image to Image",
|
||||
"api_luma_photon_style_ref": "Luma Photon Style Reference",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft Color Control Image Generation",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft Style Control Image Generation",
|
||||
"api_recraft_vector_gen": "Recraft Vector Generation"
|
||||
},
|
||||
"Video API": {
|
||||
"api_luma_i2v": "Luma Image to Video",
|
||||
"api_kling_i2v": "Kling Image to Video",
|
||||
"api_veo2_i2v": "Veo2 Image to Video",
|
||||
"api_hailuo_minimax_i2v": "MiniMax Image to Video",
|
||||
"api_pika_scene": "Pika Scenes: Images to Video",
|
||||
"api_pixverse_template_i2v": "PixVerse Template Effects: Image to Video",
|
||||
"api_pixverse_t2v": "PixVerse Text to Video"
|
||||
"Flux": {
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev(Basic)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev(Grouped)",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8",
|
||||
"flux_schnell": "Flux Schnell fp8",
|
||||
"flux_dev_full_text_to_image": "Flux Dev full text to image",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell full text to image",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_canny_model_example": "Flux Canny Model",
|
||||
"flux_depth_lora_example": "Flux Depth LoRA",
|
||||
"flux_redux_model_example": "Flux Redux Model"
|
||||
},
|
||||
"Image": {
|
||||
"image_omnigen2_t2i": "OmniGen2 Text to Image",
|
||||
"image_omnigen2_image_edit": "OmniGen2 Image Edit",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B T2I",
|
||||
"image_chroma_text_to_image": "Chroma text to image",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full",
|
||||
"hidream_e1_full": "HiDream E1 Full",
|
||||
"sd3_5_simple_example": "SD3.5 Simple",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Large Canny ControlNet",
|
||||
"sd3_5_large_depth": "SD3.5 Large Depth",
|
||||
@@ -732,23 +745,114 @@
|
||||
"sdxl_revision_text_prompts": "SDXL Revision Text Prompts",
|
||||
"sdxl_revision_zero_positive": "SDXL Revision Zero Positive",
|
||||
"sdxlturbo_example": "SDXL Turbo",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full"
|
||||
"image_lotus_depth_v1_1": "Lotus Depth"
|
||||
},
|
||||
"Video": {
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE Text to Video",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE Reference to Video",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE Control Video",
|
||||
"video_wan_vace_outpainting": "Wan VACE Outpainting",
|
||||
"video_wan_vace_flf2v": "Wan VACE First-Last Frame",
|
||||
"video_wan_vace_inpainting": "Wan VACE Inpainting",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"text_to_video_wan": "Wan 2.1 Text to Video",
|
||||
"image_to_video_wan": "Wan 2.1 Image to Video",
|
||||
"wan2_1_fun_inp": "Wan 2.1 Inpainting",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"ltxv_text_to_video": "LTXV Text to Video",
|
||||
"ltxv_image_to_video": "LTXV Image to Video",
|
||||
"mochi_text_to_video_example": "Mochi Text to Video",
|
||||
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video",
|
||||
"image_to_video": "SVD Image to Video",
|
||||
"txt_to_image_to_video": "SVD Text to Image to Video"
|
||||
},
|
||||
"Image API": {
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext Multiple Image Input",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext Pro",
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext Max",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]: Text to Image",
|
||||
"api_luma_photon_i2i": "Luma Photon: Image to Image",
|
||||
"api_luma_photon_style_ref": "Luma Photon: Style Reference",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft: Color Control Image Generation",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft: Style Control Image Generation",
|
||||
"api_recraft_vector_gen": "Recraft: Vector Generation",
|
||||
"api_runway_text_to_image": "Runway: Text to Image",
|
||||
"api_runway_reference_to_image": "Runway: Reference to Image",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI: Stable Image Ultra Text to Image",
|
||||
"api_stability_ai_i2i": "Stability AI: Image to Image",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI: SD3.5 Text to Image",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI: SD3.5 Image to Image",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3: Text to Image",
|
||||
"api_openai_image_1_t2i": "OpenAI: GPT-Image-1 Text to Image",
|
||||
"api_openai_image_1_i2i": "OpenAI: GPT-Image-1 Image to Image",
|
||||
"api_openai_image_1_inpaint": "OpenAI: GPT-Image-1 Inpaint",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI: GPT-Image-1 Multi Inputs",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI: Dall-E 2 Text to Image",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI: Dall-E 2 Inpaint",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI: Dall-E 3 Text to Image"
|
||||
},
|
||||
"Video API": {
|
||||
"api_moonvalley_text_to_video": "Moonvalley: Text to Video",
|
||||
"api_moonvalley_image_to_video": "Moonvalley: Image to Video",
|
||||
"api_kling_i2v": "Kling: Image to Video",
|
||||
"api_kling_effects": "Kling: Video Effects",
|
||||
"api_kling_flf": "Kling: FLF2V",
|
||||
"api_luma_i2v": "Luma: Image to Video",
|
||||
"api_luma_t2v": "Luma: Text to Video",
|
||||
"api_hailuo_minimax_t2v": "MiniMax: Text to Video",
|
||||
"api_hailuo_minimax_i2v": "MiniMax: Image to Video",
|
||||
"api_pixverse_i2v": "PixVerse: Image to Video",
|
||||
"api_pixverse_template_i2v": "PixVerse Templates: Image to Video",
|
||||
"api_pixverse_t2v": "PixVerse: Text to Video",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway: Gen3a Turbo Image to Video",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway: Gen4 Turbo Image to Video",
|
||||
"api_runway_first_last_frame": "Runway: First Last Frame to Video",
|
||||
"api_pika_i2v": "Pika: Image to Video",
|
||||
"api_pika_scene": "Pika Scenes: Images to Video",
|
||||
"api_veo2_i2v": "Veo2: Image to Video"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin: Image to Model",
|
||||
"api_rodin_multiview_to_model": "Rodin: Multiview to Model",
|
||||
"api_tripo_text_to_model": "Tripo: Text to Model",
|
||||
"api_tripo_image_to_model": "Tripo: Image to Model",
|
||||
"api_tripo_multiview_to_model": "Tripo: Multiview to Model"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_openai_chat": "OpenAI: Chat",
|
||||
"api_google_gemini": "Google Gemini: Chat"
|
||||
},
|
||||
"Upscaling": {
|
||||
"hiresfix_latent_workflow": "Upscale",
|
||||
"esrgan_example": "ESRGAN",
|
||||
"hiresfix_esrgan_workflow": "HiresFix ESRGAN Workflow",
|
||||
"latent_upscale_different_prompt_model": "Latent Upscale Different Prompt Model"
|
||||
},
|
||||
"ControlNet": {
|
||||
"controlnet_example": "Scribble ControlNet",
|
||||
"2_pass_pose_worship": "Pose ControlNet 2 Pass",
|
||||
"depth_controlnet": "Depth ControlNet",
|
||||
"depth_t2i_adapter": "Depth T2I Adapter",
|
||||
"mixing_controlnets": "Mixing ControlNets"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Area Composition",
|
||||
"area_composition_reversed": "Area Composition Reversed",
|
||||
"area_composition_square_area_for_subject": "Area Composition Square Area for Subject"
|
||||
},
|
||||
"3D": {
|
||||
"stable_zero123_example": "Stable Zero123",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D 2.0",
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D 2.0 MV",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D 2.0 MV Turbo"
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D 2.0 MV",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D 2.0 MV Turbo",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Stable Audio"
|
||||
"audio_stable_audio_example": "Stable Audio",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1 Text to Instrumentals Music",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1 Text to Song",
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 M2M Editing"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -322,6 +322,7 @@
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "Use new menu",
|
||||
"tooltip": "Menu bar position. On mobile devices, the menu is always shown at the top.",
|
||||
"options": {
|
||||
"Disabled": "Disabled",
|
||||
"Top": "Top",
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
"noTasksFoundMessage": "No hay tareas en la cola.",
|
||||
"noWorkflowsFound": "No se encontraron flujos de trabajo.",
|
||||
"nodes": "Nodos",
|
||||
"nodesRunning": "nodos en ejecución",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Abrir nuevo problema",
|
||||
"overwrite": "Sobrescribir",
|
||||
@@ -1200,34 +1201,44 @@
|
||||
"Flux": "Flux",
|
||||
"Image": "Imagen",
|
||||
"Image API": "API de Imagen",
|
||||
"LLM API": "API LLM",
|
||||
"Upscaling": "Ampliación",
|
||||
"Video": "Video",
|
||||
"Video API": "API de Video"
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D 2.0 MV",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D 2.0 MV Turbo",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D 2.0",
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D 2.0 MV",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D 2.0 MV Turbo",
|
||||
"stable_zero123_example": "Estable Zero123"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin: Imagen a Modelo",
|
||||
"api_rodin_multiview_to_model": "Rodin: Multivista a Modelo",
|
||||
"api_tripo_image_to_model": "Tripo: Imagen a Modelo",
|
||||
"api_tripo_multiview_to_model": "Tripo: Multivista a Modelo",
|
||||
"api_tripo_text_to_model": "Tripo: Texto a Modelo"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Composición de Área",
|
||||
"area_composition_reversed": "Composición de Área Invertida",
|
||||
"area_composition_square_area_for_subject": "Composición de Área Cuadrada para el Sujeto"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Audio Estable"
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 Edición M2M",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1 Texto a Música Instrumental",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1 Texto a Canción",
|
||||
"audio_stable_audio_example": "Stable Audio"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "Generación de Imagen",
|
||||
"embedding_example": "Incrustación",
|
||||
"gligen_textbox_example": "Caja de Texto Gligen",
|
||||
"image2image": "Imagen a Imagen",
|
||||
"inpain_model_outpainting": "Outpaint",
|
||||
"inpaint_example": "Inpaint",
|
||||
"lora": "Lora",
|
||||
"lora_multiple": "Lora Múltiple"
|
||||
"inpaint_model_outpainting": "Outpaint",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "LoRA Múltiple"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "ControlNet de Pose 2 Pasadas",
|
||||
@@ -1238,17 +1249,27 @@
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Flux Canny Model",
|
||||
"flux_depth_lora_example": "Flux Depth Lora",
|
||||
"flux_dev_checkpoint_example": "Flux Dev",
|
||||
"flux_depth_lora_example": "Flux Depth LoRA",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8",
|
||||
"flux_dev_full_text_to_image": "Flux Dev texto a imagen completo",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev (Básico)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev (Agrupado)",
|
||||
"flux_redux_model_example": "Flux Redux Model",
|
||||
"flux_schnell": "Flux Schnell"
|
||||
"flux_schnell": "Flux Schnell fp8",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell texto a imagen completo"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 Completo",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Rápido",
|
||||
"hidream_i1_full": "HiDream I1 Completo",
|
||||
"image_chroma_text_to_image": "Chroma texto a imagen",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B T2I",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth",
|
||||
"image_omnigen2_image_edit": "OmniGen2 Edición de Imagen",
|
||||
"image_omnigen2_t2i": "OmniGen2 Texto a Imagen",
|
||||
"sd3_5_large_blur": "SD3.5 Grande Desenfoque",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Grande Canny ControlNet",
|
||||
"sd3_5_large_depth": "SD3.5 Grande Profundidad",
|
||||
@@ -1260,21 +1281,33 @@
|
||||
"sdxlturbo_example": "SDXL Turbo"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 Rellenar",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 Texto a Imagen",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 Texto a Imagen",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra Texto a Imagen",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 Texto a Imagen",
|
||||
"api_luma_photon_i2i": "Luma Photon Imagen a Imagen",
|
||||
"api_luma_photon_style_ref": "Luma Photon Referencia de Estilo",
|
||||
"api_openai_image_1_i2i": "OpenAI Image-1 Imagen a Imagen",
|
||||
"api_openai_image_1_inpaint": "OpenAI Image-1 Rellenar",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI Image-1 Múltiples Entradas",
|
||||
"api_openai_image_1_t2i": "OpenAI Image-1 Texto a Imagen",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft Generación de Imagen con Control de Color",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft Generación de Imagen con Control de Estilo",
|
||||
"api_recraft_vector_gen": "Recraft Generación de Vectores",
|
||||
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra Texto a Imagen"
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext Max",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext Entrada de Múltiples Imágenes",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext Pro",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]: Texto a Imagen",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3: Texto a Imagen",
|
||||
"api_luma_photon_i2i": "Luma Photon: Imagen a Imagen",
|
||||
"api_luma_photon_style_ref": "Luma Photon: Referencia de Estilo",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI: Dall-E 2 Rellenar",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI: Dall-E 2 Texto a Imagen",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI: Dall-E 3 Texto a Imagen",
|
||||
"api_openai_image_1_i2i": "OpenAI: GPT-Image-1 Imagen a Imagen",
|
||||
"api_openai_image_1_inpaint": "OpenAI: GPT-Image-1 Rellenar",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI: GPT-Image-1 Múltiples Entradas",
|
||||
"api_openai_image_1_t2i": "OpenAI: GPT-Image-1 Texto a Imagen",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft: Generación de Imagen con Control de Color",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft: Generación de Imagen con Control de Estilo",
|
||||
"api_recraft_vector_gen": "Recraft: Generación de Vectores",
|
||||
"api_runway_reference_to_image": "Runway: Referencia a Imagen",
|
||||
"api_runway_text_to_image": "Runway: Texto a Imagen",
|
||||
"api_stability_ai_i2i": "Stability AI: Imagen a Imagen",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI: SD3.5 Imagen a Imagen",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI: SD3.5 Texto a Imagen",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI: Stable Image Ultra Texto a Imagen"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google Gemini: Chat",
|
||||
"api_openai_chat": "OpenAI: Chat"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "ESRGAN",
|
||||
@@ -1291,119 +1324,190 @@
|
||||
"mochi_text_to_video_example": "Mochi Texto a Video",
|
||||
"text_to_video_wan": "Wan 2.1 Texto a Video",
|
||||
"txt_to_image_to_video": "SVD Texto a Imagen a Video",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE Referencia a Video",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE Texto a Video",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE Control Video",
|
||||
"video_wan_vace_flf2v": "Wan VACE Primer-Ultimo Fotograma",
|
||||
"video_wan_vace_inpainting": "Wan VACE Inpainting",
|
||||
"video_wan_vace_outpainting": "Wan VACE Outpainting",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet",
|
||||
"wan2_1_fun_inp": "Wan 2.1 Relleno"
|
||||
"wan2_1_fun_inp": "Wan 2.1 Inpainting"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "MiniMax Imagen a Video",
|
||||
"api_kling_i2v": "Kling Imagen a Video",
|
||||
"api_luma_i2v": "Luma Imagen a Video",
|
||||
"api_hailuo_minimax_i2v": "MiniMax: Imagen a Video",
|
||||
"api_hailuo_minimax_t2v": "MiniMax: Texto a Video",
|
||||
"api_kling_effects": "Kling: Efectos de Video",
|
||||
"api_kling_flf": "Kling: FLF2V",
|
||||
"api_kling_i2v": "Kling: Imagen a Video",
|
||||
"api_luma_i2v": "Luma: Imagen a Video",
|
||||
"api_luma_t2v": "Luma: Texto a Video",
|
||||
"api_moonvalley_image_to_video": "Moonvalley: Imagen a Video",
|
||||
"api_moonvalley_text_to_video": "Moonvalley: Texto a Video",
|
||||
"api_pika_i2v": "Pika: Imagen a Video",
|
||||
"api_pika_scene": "Pika Escenas: Imágenes a Video",
|
||||
"api_pixverse_t2v": "PixVerse Texto a Video",
|
||||
"api_pixverse_template_i2v": "PixVerse Template Effects: Imagen a Video",
|
||||
"api_veo2_i2v": "Veo2 Imagen a Video"
|
||||
"api_pixverse_i2v": "PixVerse: Imagen a Video",
|
||||
"api_pixverse_t2v": "PixVerse: Texto a Video",
|
||||
"api_pixverse_template_i2v": "PixVerse Plantillas: Imagen a Video",
|
||||
"api_runway_first_last_frame": "Runway: Primer Último Fotograma a Video",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway: Gen3a Turbo Imagen a Video",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway: Gen4 Turbo Imagen a Video",
|
||||
"api_veo2_i2v": "Veo2: Imagen a Video"
|
||||
}
|
||||
},
|
||||
"templateDescription": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Usa Hunyuan3D 2mv para generar modelos desde múltiples vistas.",
|
||||
"hunyuan-3d-turbo": "Usa Hunyuan3D 2mv turbo para generar modelos desde múltiples vistas.",
|
||||
"hunyuan3d-non-multiview-train": "Usa Hunyuan3D 2.0 para generar modelos desde una sola vista.",
|
||||
"stable_zero123_example": "Genera vistas 3D a partir de imágenes individuales."
|
||||
"3d_hunyuan3d_image_to_model": "Genera modelos 3D a partir de imágenes individuales usando Hunyuan3D 2.0.",
|
||||
"3d_hunyuan3d_multiview_to_model": "Genera modelos 3D a partir de múltiples vistas usando Hunyuan3D 2.0 MV.",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Genera modelos 3D a partir de múltiples vistas usando Hunyuan3D 2.0 MV Turbo.",
|
||||
"stable_zero123_example": "Genera vistas 3D a partir de imágenes individuales usando Stable Zero123."
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Genera modelos 3D detallados a partir de una sola foto usando Rodin AI.",
|
||||
"api_rodin_multiview_to_model": "Esculpe modelos 3D completos usando reconstrucción multivista de Rodin.",
|
||||
"api_tripo_image_to_model": "Genera activos 3D profesionales a partir de imágenes 2D usando el motor Tripo.",
|
||||
"api_tripo_multiview_to_model": "Construye modelos 3D a partir de múltiples ángulos con el escáner avanzado de Tripo.",
|
||||
"api_tripo_text_to_model": "Crea objetos 3D a partir de descripciones con modelado basado en texto de Tripo."
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Controla la composición de la imagen por áreas.",
|
||||
"area_composition_reversed": "Invierte el flujo de composición por áreas.",
|
||||
"area_composition_square_area_for_subject": "Crea una colocación consistente del sujeto."
|
||||
"area_composition": "Genera imágenes controlando la composición con áreas definidas.",
|
||||
"area_composition_square_area_for_subject": "Genera imágenes con colocación consistente del sujeto usando composición de áreas."
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Genera audio a partir de descripciones de texto."
|
||||
"audio_ace_step_1_m2m_editing": "Edita canciones existentes para cambiar el estilo y la letra usando ACE-Step v1 M2M.",
|
||||
"audio_ace_step_1_t2a_instrumentals": "Genera música instrumental a partir de texto usando ACE-Step v1.",
|
||||
"audio_ace_step_1_t2a_song": "Genera canciones con voz a partir de texto usando ACE-Step v1, soportando múltiples idiomas y personalización de estilo.",
|
||||
"audio_stable_audio_example": "Genera audio a partir de descripciones de texto usando Stable Audio."
|
||||
},
|
||||
"Basics": {
|
||||
"default": "Genera imágenes a partir de descripciones de texto.",
|
||||
"embedding_example": "Utiliza inversión textual para estilos consistentes.",
|
||||
"gligen_textbox_example": "Especifica la ubicación y el tamaño de los objetos.",
|
||||
"embedding_example": "Genera imágenes usando inversión textual para estilos consistentes.",
|
||||
"gligen_textbox_example": "Genera imágenes con colocación precisa de objetos usando cajas de texto.",
|
||||
"image2image": "Transforma imágenes existentes usando indicaciones de texto.",
|
||||
"inpain_model_outpainting": "Extiende imágenes más allá de sus límites originales.",
|
||||
"inpaint_example": "Edita partes específicas de imágenes de manera fluida.",
|
||||
"lora": "Aplica modelos LoRA para estilos o temas especializados.",
|
||||
"lora_multiple": "Combina múltiples modelos LoRA para resultados únicos."
|
||||
"inpaint_model_outpainting": "Extiende imágenes más allá de sus límites originales.",
|
||||
"lora": "Genera imágenes con modelos LoRA para estilos o temas especializados.",
|
||||
"lora_multiple": "Genera imágenes combinando múltiples modelos LoRA."
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "Genera imágenes a partir de referencias de pose.",
|
||||
"controlnet_example": "Controla la generación de imágenes con imágenes de referencia.",
|
||||
"depth_controlnet": "Crea imágenes con generación consciente de profundidad.",
|
||||
"depth_t2i_adapter": "Genera rápidamente imágenes conscientes de profundidad con un adaptador T2I.",
|
||||
"mixing_controlnets": "Combina múltiples modelos ControlNet juntos."
|
||||
"2_pass_pose_worship": "Genera imágenes guiadas por referencias de pose usando ControlNet.",
|
||||
"controlnet_example": "Genera imágenes guiadas por imágenes de garabato usando ControlNet.",
|
||||
"depth_controlnet": "Genera imágenes guiadas por información de profundidad usando ControlNet.",
|
||||
"depth_t2i_adapter": "Genera imágenes guiadas por información de profundidad usando el adaptador T2I.",
|
||||
"mixing_controlnets": "Genera imágenes combinando múltiples modelos ControlNet."
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Genera imágenes a partir de detección de bordes.",
|
||||
"flux_depth_lora_example": "Crea imágenes con LoRA consciente de profundidad.",
|
||||
"flux_dev_checkpoint_example": "Crea imágenes usando modelos de desarrollo de Flux.",
|
||||
"flux_fill_inpaint_example": "Rellena partes faltantes de imágenes.",
|
||||
"flux_fill_outpaint_example": "Extiende imágenes usando outpainting de Flux.",
|
||||
"flux_redux_model_example": "Transfiere el estilo de una imagen de referencia para guiar la generación de imágenes con Flux.",
|
||||
"flux_schnell": "Genera imágenes rápidamente con Flux Schnell."
|
||||
"flux_canny_model_example": "Genera imágenes guiadas por detección de bordes usando Flux Canny.",
|
||||
"flux_depth_lora_example": "Genera imágenes guiadas por información de profundidad usando Flux LoRA.",
|
||||
"flux_dev_checkpoint_example": "Genera imágenes usando la versión cuantizada fp8 de Flux Dev. Ideal para dispositivos con poca VRAM, solo requiere un archivo de modelo, pero la calidad es ligeramente inferior a la versión completa.",
|
||||
"flux_dev_full_text_to_image": "Genera imágenes de alta calidad con la versión completa de Flux Dev. Requiere más VRAM y múltiples archivos de modelo, pero ofrece la mejor adherencia a la indicación y calidad de imagen.",
|
||||
"flux_fill_inpaint_example": "Rellena partes faltantes de imágenes usando inpainting de Flux.",
|
||||
"flux_fill_outpaint_example": "Extiende imágenes más allá de los límites usando outpainting de Flux.",
|
||||
"flux_kontext_dev_basic": "Edita imágenes usando Flux Kontext con visibilidad total de nodos, ideal para aprender el flujo de trabajo.",
|
||||
"flux_kontext_dev_grouped": "Versión simplificada de Flux Kontext con nodos agrupados para un espacio de trabajo más limpio.",
|
||||
"flux_redux_model_example": "Genera imágenes transfiriendo el estilo de imágenes de referencia usando Flux Redux.",
|
||||
"flux_schnell": "Genera imágenes rápidamente con la versión cuantizada fp8 de Flux Schnell. Perfecto para hardware de gama baja, solo requiere 4 pasos.",
|
||||
"flux_schnell_full_text_to_image": "Genera imágenes rápidamente con la versión completa de Flux Schnell. Licencia Apache2.0, solo requiere 4 pasos manteniendo buena calidad."
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "Edita imágenes con HiDream E1.",
|
||||
"hidream_i1_dev": "Genera imágenes con HiDream I1 Dev.",
|
||||
"hidream_i1_fast": "Genera imágenes rápidamente con HiDream I1.",
|
||||
"hidream_i1_full": "Genera imágenes con HiDream I1.",
|
||||
"sd3_5_large_blur": "Genera imágenes a partir de imágenes de referencia borrosas con SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Usa detección de bordes para guiar la generación de imágenes con SD 3.5.",
|
||||
"sd3_5_large_depth": "Crea imágenes conscientes de profundidad con SD 3.5.",
|
||||
"sd3_5_simple_example": "Genera imágenes con SD 3.5.",
|
||||
"sdxl_refiner_prompt_example": "Mejora los resultados de SDXL con refinadores.",
|
||||
"sdxl_revision_text_prompts": "Transfiere conceptos de imágenes de referencia para guiar la generación de imágenes con SDXL.",
|
||||
"sdxl_revision_zero_positive": "Agrega indicaciones de texto junto a imágenes de referencia para guiar la generación de imágenes con SDXL.",
|
||||
"sdxl_simple_example": "Crea imágenes de alta calidad con SDXL.",
|
||||
"sdxlturbo_example": "Genera imágenes en un solo paso con SDXL Turbo."
|
||||
"hidream_e1_full": "Edita imágenes con HiDream E1 - Modelo profesional de edición de imágenes por lenguaje natural.",
|
||||
"hidream_i1_dev": "Genera imágenes con HiDream I1 Dev - Versión equilibrada con 28 pasos de inferencia, adecuada para hardware medio.",
|
||||
"hidream_i1_fast": "Genera imágenes rápidamente con HiDream I1 Fast - Versión ligera con 16 pasos, ideal para previsualizaciones rápidas.",
|
||||
"hidream_i1_full": "Genera imágenes con HiDream I1 Full - Versión completa con 50 pasos para la máxima calidad.",
|
||||
"image_chroma_text_to_image": "Chroma está modificado de Flux y tiene algunos cambios en la arquitectura.",
|
||||
"image_cosmos_predict2_2B_t2i": "Genera imágenes con Cosmos-Predict2 2B T2I, logrando generación física precisa, alta fidelidad y gran detalle.",
|
||||
"image_lotus_depth_v1_1": "Ejecuta Lotus Depth en ComfyUI para estimación de profundidad monocular eficiente y detallada.",
|
||||
"image_omnigen2_image_edit": "Edita imágenes con instrucciones en lenguaje natural usando las avanzadas capacidades de edición de imagen y soporte de texto de OmniGen2.",
|
||||
"image_omnigen2_t2i": "Genera imágenes de alta calidad a partir de texto usando el modelo multimodal unificado 7B de OmniGen2 con arquitectura de doble vía.",
|
||||
"sd3_5_large_blur": "Genera imágenes guiadas por imágenes de referencia borrosas usando SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Genera imágenes guiadas por detección de bordes usando SD 3.5 Canny ControlNet.",
|
||||
"sd3_5_large_depth": "Genera imágenes guiadas por información de profundidad usando SD 3.5.",
|
||||
"sd3_5_simple_example": "Genera imágenes usando SD 3.5.",
|
||||
"sdxl_refiner_prompt_example": "Mejora imágenes SDXL usando modelos refinadores.",
|
||||
"sdxl_revision_text_prompts": "Genera imágenes transfiriendo conceptos de imágenes de referencia usando SDXL Revision.",
|
||||
"sdxl_revision_zero_positive": "Genera imágenes usando tanto indicaciones de texto como imágenes de referencia con SDXL Revision.",
|
||||
"sdxl_simple_example": "Genera imágenes de alta calidad usando SDXL.",
|
||||
"sdxlturbo_example": "Genera imágenes en un solo paso usando SDXL Turbo."
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Usa la API Dall-E 2 para hacer inpainting en imágenes.",
|
||||
"api-openai-dall-e-2-t2i": "Usa la API Dall-E 2 para generar imágenes a partir de descripciones de texto.",
|
||||
"api-openai-dall-e-3-t2i": "Usa la API Dall-E 3 para generar imágenes a partir de descripciones de texto.",
|
||||
"api_bfl_flux_pro_t2i": "Crea imágenes con FLUX.1 [pro] y su excelente seguimiento de indicaciones, calidad visual, detalle de imagen y diversidad de resultados.",
|
||||
"api_ideogram_v3_t2i": "Genera imágenes con alineación de indicaciones de alta calidad, fotorrealismo y renderizado de texto. Crea logotipos de calidad profesional, carteles promocionales, conceptos de páginas de destino, fotografía de productos y más. Crea composiciones espaciales sofisticadas con fondos intrincados, iluminación y colores precisos y matizados, y detalles ambientales realistas.",
|
||||
"api_bfl_flux_1_kontext_max_image": "Edita imágenes con Flux.1 Kontext max image.",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "Introduce múltiples imágenes y edítalas con Flux.1 Kontext.",
|
||||
"api_bfl_flux_1_kontext_pro_image": "Edita imágenes con Flux.1 Kontext pro image.",
|
||||
"api_bfl_flux_pro_t2i": "Genera imágenes con excelente seguimiento de indicaciones y calidad visual usando FLUX.1 Pro.",
|
||||
"api_ideogram_v3_t2i": "Genera imágenes de calidad profesional con excelente alineación de indicaciones, fotorrealismo y renderizado de texto usando Ideogram V3.",
|
||||
"api_luma_photon_i2i": "Guía la generación de imágenes usando una combinación de imágenes e indicaciones.",
|
||||
"api_luma_photon_style_ref": "Aplica y combina referencias de estilo con control exacto. Luma Photon captura la esencia de cada imagen de referencia, permitiéndote combinar elementos visuales distintos manteniendo calidad profesional.",
|
||||
"api_openai_image_1_i2i": "Usa la API GPT Image 1 para generar imágenes a partir de imágenes.",
|
||||
"api_openai_image_1_inpaint": "Usa la API GPT Image 1 para hacer inpainting en imágenes.",
|
||||
"api_openai_image_1_multi_inputs": "Usa la API GPT Image 1 con múltiples entradas para generar imágenes.",
|
||||
"api_openai_image_1_t2i": "Usa la API GPT Image 1 para generar imágenes a partir de descripciones de texto.",
|
||||
"api_recraft_image_gen_with_color_control": "Crea una paleta personalizada para reutilizar en múltiples imágenes o selecciona colores para cada foto. Haz coincidir la paleta de tu marca y crea imágenes visuales que sean distintivamente tuyas.",
|
||||
"api_recraft_image_gen_with_style_control": "Controla el estilo con ejemplos visuales, alinea la posición y ajusta objetos. Guarda y comparte estilos para una consistencia perfecta de marca.",
|
||||
"api_recraft_vector_gen": "Pasa de una indicación de texto a una imagen vectorial con el generador de vectores IA de Recraft. Produce arte vectorial de la mejor calidad para logotipos, carteles, conjuntos de iconos, anuncios, banners y maquetas. Perfecciona tus diseños con archivos SVG nítidos y de alta calidad. Crea ilustraciones vectoriales de marca para tu app o sitio web en segundos.",
|
||||
"api_stability_sd3_t2i": "Genera imágenes de alta calidad con excelente adherencia a las indicaciones. Perfecto para casos de uso profesional a resolución de 1 megapíxel."
|
||||
"api_luma_photon_style_ref": "Genera imágenes combinando referencias de estilo con control preciso usando Luma Photon.",
|
||||
"api_openai_dall_e_2_inpaint": "Edita imágenes usando inpainting con la API OpenAI Dall-E 2.",
|
||||
"api_openai_dall_e_2_t2i": "Genera imágenes a partir de texto usando la API OpenAI Dall-E 2.",
|
||||
"api_openai_dall_e_3_t2i": "Genera imágenes a partir de texto usando la API OpenAI Dall-E 3.",
|
||||
"api_openai_image_1_i2i": "Genera imágenes a partir de imágenes usando la API OpenAI GPT Image 1.",
|
||||
"api_openai_image_1_inpaint": "Edita imágenes usando inpainting con la API OpenAI GPT Image 1.",
|
||||
"api_openai_image_1_multi_inputs": "Genera imágenes a partir de múltiples entradas usando la API OpenAI GPT Image 1.",
|
||||
"api_openai_image_1_t2i": "Genera imágenes a partir de texto usando la API OpenAI GPT Image 1.",
|
||||
"api_recraft_image_gen_with_color_control": "Genera imágenes con paletas de color personalizadas y visuales de marca usando Recraft.",
|
||||
"api_recraft_image_gen_with_style_control": "Controla el estilo con ejemplos visuales, alinea la posición y ajusta objetos. Guarda y comparte estilos para consistencia de marca.",
|
||||
"api_recraft_vector_gen": "Genera imágenes vectoriales de alta calidad a partir de texto usando el generador de vectores IA de Recraft.",
|
||||
"api_runway_reference_to_image": "Genera nuevas imágenes basadas en estilos y composiciones de referencia con Runway.",
|
||||
"api_runway_text_to_image": "Genera imágenes de alta calidad a partir de texto usando el modelo IA de Runway.",
|
||||
"api_stability_ai_i2i": "Transforma imágenes con generación de alta calidad usando Stability AI, ideal para edición profesional y transferencia de estilo.",
|
||||
"api_stability_ai_sd3_5_i2i": "Genera imágenes de alta calidad con excelente adherencia a la indicación. Perfecto para uso profesional a 1 megapíxel.",
|
||||
"api_stability_ai_sd3_5_t2i": "Genera imágenes de alta calidad con excelente adherencia a la indicación. Perfecto para uso profesional a 1 megapíxel.",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Genera imágenes de alta calidad con excelente adherencia a la indicación. Perfecto para uso profesional a 1 megapíxel."
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Experimenta la IA multimodal de Google con las capacidades de razonamiento de Gemini.",
|
||||
"api_openai_chat": "Interactúa con los avanzados modelos de lenguaje de OpenAI para conversaciones inteligentes."
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "Usa modelos de escalado para mejorar la calidad de imagen.",
|
||||
"hiresfix_esrgan_workflow": "Usa modelos de escalado durante pasos intermedios.",
|
||||
"hiresfix_latent_workflow": "Mejora la calidad de imagen en el espacio latente.",
|
||||
"latent_upscale_different_prompt_model": "Escala y cambia la indicación entre pasadas."
|
||||
"esrgan_example": "Escala imágenes usando modelos ESRGAN para mejorar la calidad.",
|
||||
"hiresfix_esrgan_workflow": "Escala imágenes usando modelos ESRGAN durante pasos intermedios.",
|
||||
"hiresfix_latent_workflow": "Escala imágenes mejorando la calidad en el espacio latente.",
|
||||
"latent_upscale_different_prompt_model": "Escala imágenes cambiando las indicaciones entre pasadas."
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Genera videos usando el modelo Hunyuan.",
|
||||
"image_to_video": "Transforma imágenes en videos animados.",
|
||||
"image_to_video_wan": "Genera videos rápidamente a partir de imágenes.",
|
||||
"ltxv_image_to_video": "Convierte imágenes fijas en videos.",
|
||||
"ltxv_text_to_video": "Genera videos a partir de descripciones de texto.",
|
||||
"mochi_text_to_video_example": "Crea videos con el modelo Mochi.",
|
||||
"text_to_video_wan": "Genera videos rápidamente a partir de descripciones de texto.",
|
||||
"txt_to_image_to_video": "Genera imágenes a partir de texto y luego conviértelas en videos.",
|
||||
"wan2_1_flf2v_720_f16": "Genera video controlando el primer y último fotograma.",
|
||||
"wan2_1_fun_control": "Guía la generación de video con pose, profundidad, controles de bordes y más.",
|
||||
"wan2_1_fun_inp": "Crea videos a partir de fotogramas iniciales y finales."
|
||||
"hunyuan_video_text_to_video": "Genera videos a partir de texto usando el modelo Hunyuan.",
|
||||
"image_to_video": "Genera videos a partir de imágenes fijas.",
|
||||
"image_to_video_wan": "Genera videos a partir de imágenes usando Wan 2.1.",
|
||||
"ltxv_image_to_video": "Genera videos a partir de imágenes fijas.",
|
||||
"ltxv_text_to_video": "Genera videos a partir de texto.",
|
||||
"mochi_text_to_video_example": "Genera videos a partir de texto usando el modelo Mochi.",
|
||||
"text_to_video_wan": "Genera videos a partir de texto usando Wan 2.1.",
|
||||
"txt_to_image_to_video": "Genera videos creando primero imágenes a partir de texto.",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Genera videos con Cosmos-Predict2 2B Video2World, logrando simulaciones físicas precisas, alta fidelidad y consistencia.",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Genera videos de alta calidad con control avanzado de cámara usando el modelo completo de 14B.",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Genera videos dinámicos con movimientos de cámara cinematográficos usando Wan 2.1 Fun Camera 1.3B.",
|
||||
"video_wan_vace_14B_ref2v": "Crea videos que coinciden con el estilo y contenido de una imagen de referencia.",
|
||||
"video_wan_vace_14B_t2v": "Transforma descripciones de texto en videos de alta calidad. Soporta 480p y 720p con el modelo VACE-14B.",
|
||||
"video_wan_vace_14B_v2v": "Genera videos controlando videos de entrada e imágenes de referencia usando Wan VACE.",
|
||||
"video_wan_vace_flf2v": "Genera transiciones suaves definiendo fotogramas iniciales y finales. Soporta secuencias de fotogramas personalizadas.",
|
||||
"video_wan_vace_inpainting": "Edita regiones específicas en videos preservando el contenido circundante.",
|
||||
"video_wan_vace_outpainting": "Genera videos extendidos expandiendo el tamaño usando Wan VACE outpainting.",
|
||||
"wan2_1_flf2v_720_f16": "Genera videos controlando primer y último fotograma usando Wan 2.1 FLF2V.",
|
||||
"wan2_1_fun_control": "Genera videos guiados por pose, profundidad y bordes usando Wan 2.1 ControlNet.",
|
||||
"wan2_1_fun_inp": "Genera videos a partir de fotogramas iniciales y finales usando Wan 2.1 inpainting."
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "Crea videos refinados a partir de imágenes y texto, incluyendo integración CGI y efectos fotográficos de tendencia como abrazos virales de IA. Elige entre una variedad de estilos y temas de video para que coincidan con tu visión creativa.",
|
||||
"api_kling_i2v": "Crea videos con gran adherencia a las indicaciones para acciones, expresiones y movimientos de cámara. Ahora soporta indicaciones complejas con acciones secuenciales para que seas el director de tu escena.",
|
||||
"api_hailuo_minimax_i2v": "Genera videos refinados a partir de imágenes y texto con integración CGI usando MiniMax.",
|
||||
"api_hailuo_minimax_t2v": "Genera videos de alta calidad directamente desde texto. Explora las capacidades avanzadas de IA de MiniMax para crear narrativas visuales diversas con efectos CGI profesionales.",
|
||||
"api_kling_effects": "Genera videos dinámicos aplicando efectos visuales a imágenes usando Kling.",
|
||||
"api_kling_flf": "Genera videos controlando los primeros y últimos fotogramas.",
|
||||
"api_kling_i2v": "Genera videos con excelente adherencia a la indicación para acciones, expresiones y movimientos de cámara usando Kling.",
|
||||
"api_luma_i2v": "Convierte imágenes estáticas en animaciones mágicas de alta calidad al instante.",
|
||||
"api_pika_scene": "Usa múltiples imágenes como ingredientes y genera videos que las incorporen todas.",
|
||||
"api_pixverse_t2v": "Genera videos con interpretación precisa de indicaciones y una dinámica visual impresionante.",
|
||||
"api_pixverse_template_i2v": "Transforma imágenes estáticas en videos dinámicos con movimiento y efectos.",
|
||||
"api_veo2_i2v": "Usa la API Google Veo2 para generar videos a partir de imágenes."
|
||||
"api_luma_t2v": "Genera videos de alta calidad usando indicaciones simples.",
|
||||
"api_moonvalley_image_to_video": "Genera videos cinematográficos 1080p a partir de una imagen usando un modelo entrenado solo con datos licenciados.",
|
||||
"api_moonvalley_text_to_video": "Genera videos cinematográficos 1080p a partir de texto usando un modelo entrenado solo con datos licenciados.",
|
||||
"api_pika_i2v": "Genera videos animados suaves a partir de imágenes estáticas usando Pika AI.",
|
||||
"api_pika_scene": "Genera videos que incorporan múltiples imágenes de entrada usando Pika Scenes.",
|
||||
"api_pixverse_i2v": "Genera videos dinámicos a partir de imágenes estáticas con movimiento y efectos usando PixVerse.",
|
||||
"api_pixverse_t2v": "Genera videos con interpretación precisa de indicaciones y dinámica visual impresionante.",
|
||||
"api_pixverse_template_i2v": "Genera videos dinámicos a partir de imágenes estáticas con movimiento y efectos usando PixVerse.",
|
||||
"api_runway_first_last_frame": "Genera transiciones de video suaves entre dos fotogramas clave con precisión de Runway.",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Genera videos cinematográficos a partir de imágenes estáticas usando Runway Gen3a Turbo.",
|
||||
"api_runway_gen4_turo_image_to_video": "Genera videos dinámicos a partir de imágenes usando Runway Gen4 Turbo.",
|
||||
"api_veo2_i2v": "Genera videos a partir de imágenes usando la API Google Veo2."
|
||||
}
|
||||
},
|
||||
"title": "Comienza con una Plantilla"
|
||||
|
||||
@@ -326,7 +326,8 @@
|
||||
"Bottom": "Abajo",
|
||||
"Disabled": "Deshabilitado",
|
||||
"Top": "Arriba"
|
||||
}
|
||||
},
|
||||
"tooltip": "Posición de la barra de menú. En dispositivos móviles, el menú siempre se muestra en la parte superior."
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "Validar definiciones de nodos (lento)",
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
"noWorkflowsFound": "Aucun flux de travail trouvé.",
|
||||
"nodes": "Nœuds",
|
||||
"nodesRunning": "nœuds en cours d’exécution",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Ouvrir un nouveau problème",
|
||||
"overwrite": "Écraser",
|
||||
@@ -1200,34 +1201,44 @@
|
||||
"Flux": "Flux",
|
||||
"Image": "Image",
|
||||
"Image API": "API d'image",
|
||||
"LLM API": "API LLM",
|
||||
"Upscaling": "Mise à l'échelle",
|
||||
"Video": "Vidéo",
|
||||
"Video API": "API vidéo"
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D Multivue",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D Turbo",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D Multivue",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D Turbo",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin : Image vers Modèle",
|
||||
"api_rodin_multiview_to_model": "Rodin : Multivue vers Modèle",
|
||||
"api_tripo_image_to_model": "Tripo : Image vers Modèle",
|
||||
"api_tripo_multiview_to_model": "Tripo : Multivue vers Modèle",
|
||||
"api_tripo_text_to_model": "Tripo : Texte vers Modèle"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Composition de Zone",
|
||||
"area_composition_reversed": "Composition de Zone Inversée",
|
||||
"area_composition_square_area_for_subject": "Composition de Zone Carrée pour le Sujet"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Stable Audio"
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 Édition M2M",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1 Texte vers Musique Instrumentale",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1 Texte vers Chanson",
|
||||
"audio_stable_audio_example": "Stable Audio"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "Génération d'Image",
|
||||
"embedding_example": "Intégration",
|
||||
"gligen_textbox_example": "Boîte de Texte Gligen",
|
||||
"image2image": "Image à Image",
|
||||
"inpain_model_outpainting": "Modèle Inpaint Outpainting",
|
||||
"inpaint_example": "Inpaint",
|
||||
"lora": "Lora",
|
||||
"lora_multiple": "Lora Multiple"
|
||||
"inpaint_model_outpainting": "Outpainting",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "LoRA Multiple"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "2 Passes Pose Worship",
|
||||
@@ -1238,17 +1249,27 @@
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Flux Canny Model",
|
||||
"flux_depth_lora_example": "Flux Depth Lora",
|
||||
"flux_dev_checkpoint_example": "Flux Dev",
|
||||
"flux_depth_lora_example": "Flux Depth LoRA",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8",
|
||||
"flux_dev_full_text_to_image": "Flux Dev texte vers image complet",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev (Basique)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev (Groupé)",
|
||||
"flux_redux_model_example": "Flux Redux Model",
|
||||
"flux_schnell": "Flux Schnell"
|
||||
"flux_schnell": "Flux Schnell fp8",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell texte vers image complet"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 Complet",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Rapide",
|
||||
"hidream_i1_full": "HiDream I1 Complet",
|
||||
"image_chroma_text_to_image": "Chroma texte vers image",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B T2I",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth",
|
||||
"image_omnigen2_image_edit": "OmniGen2 Édition d'Image",
|
||||
"image_omnigen2_t2i": "OmniGen2 Texte vers Image",
|
||||
"sd3_5_large_blur": "SD3.5 Grand Flou",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Grand Canny ControlNet",
|
||||
"sd3_5_large_depth": "SD3.5 Grande Profondeur",
|
||||
@@ -1260,21 +1281,33 @@
|
||||
"sdxlturbo_example": "SDXL Turbo"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 Inpainting",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 Texte vers Image",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 Texte vers Image",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra Texte vers Image",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 Texte vers Image",
|
||||
"api_luma_photon_i2i": "Luma Photon Image vers Image",
|
||||
"api_luma_photon_style_ref": "Luma Photon Référence de Style",
|
||||
"api_openai_image_1_i2i": "OpenAI Image-1 Image vers Image",
|
||||
"api_openai_image_1_inpaint": "OpenAI Image-1 Inpainting",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI Image-1 Entrées Multiples",
|
||||
"api_openai_image_1_t2i": "OpenAI Image-1 Texte vers Image",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft Génération d’Image avec Contrôle des Couleurs",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft Génération d’Image avec Contrôle du Style",
|
||||
"api_recraft_vector_gen": "Recraft Génération de Vecteur",
|
||||
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra Texte vers Image"
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext Max",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext Entrée Multi-Images",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext Pro",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro] : Texte vers Image",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 : Texte vers Image",
|
||||
"api_luma_photon_i2i": "Luma Photon : Image vers Image",
|
||||
"api_luma_photon_style_ref": "Luma Photon : Référence de Style",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI : Dall-E 2 Inpainting",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI : Dall-E 2 Texte vers Image",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI : Dall-E 3 Texte vers Image",
|
||||
"api_openai_image_1_i2i": "OpenAI : GPT-Image-1 Image vers Image",
|
||||
"api_openai_image_1_inpaint": "OpenAI : GPT-Image-1 Inpainting",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI : GPT-Image-1 Entrées Multiples",
|
||||
"api_openai_image_1_t2i": "OpenAI : GPT-Image-1 Texte vers Image",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft : Génération d’Image avec Contrôle des Couleurs",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft : Génération d’Image avec Contrôle du Style",
|
||||
"api_recraft_vector_gen": "Recraft : Génération de Vecteur",
|
||||
"api_runway_reference_to_image": "Runway : Référence vers Image",
|
||||
"api_runway_text_to_image": "Runway : Texte vers Image",
|
||||
"api_stability_ai_i2i": "Stability AI : Image vers Image",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI : SD3.5 Image vers Image",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI : SD3.5 Texte vers Image",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI : Stable Image Ultra Texte vers Image"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google Gemini : Chat",
|
||||
"api_openai_chat": "OpenAI : Chat"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "ESRGAN",
|
||||
@@ -1283,127 +1316,198 @@
|
||||
"latent_upscale_different_prompt_model": "Modèle d'Agrandissement Latent Différent Prompt"
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Texte à Vidéo Hunyuan",
|
||||
"image_to_video": "Image à Vidéo",
|
||||
"image_to_video_wan": "Wan 2.1 Image à Vidéo",
|
||||
"ltxv_image_to_video": "LTXV Image à Vidéo",
|
||||
"ltxv_text_to_video": "LTXV Texte à Vidéo",
|
||||
"mochi_text_to_video_example": "Exemple de Texte à Vidéo Mochi",
|
||||
"text_to_video_wan": "Wan 2.1 Texte à Vidéo",
|
||||
"txt_to_image_to_video": "Texte à Image à Vidéo",
|
||||
"hunyuan_video_text_to_video": "Hunyuan Texte vers Vidéo",
|
||||
"image_to_video": "SVD Image vers Vidéo",
|
||||
"image_to_video_wan": "Wan 2.1 Image vers Vidéo",
|
||||
"ltxv_image_to_video": "LTXV Image vers Vidéo",
|
||||
"ltxv_text_to_video": "LTXV Texte vers Vidéo",
|
||||
"mochi_text_to_video_example": "Mochi Texte vers Vidéo",
|
||||
"text_to_video_wan": "Wan 2.1 Texte vers Vidéo",
|
||||
"txt_to_image_to_video": "SVD Texte vers Image vers Vidéo",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE Référence vers Vidéo",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE Texte vers Vidéo",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE Contrôle Vidéo",
|
||||
"video_wan_vace_flf2v": "Wan VACE Premier-Dernier Frame",
|
||||
"video_wan_vace_inpainting": "Wan VACE Inpainting",
|
||||
"video_wan_vace_outpainting": "Wan VACE Outpainting",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet",
|
||||
"wan2_1_fun_inp": "Wan 2.1 Inpainting"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "MiniMax Image vers Vidéo",
|
||||
"api_kling_i2v": "Kling Image vers Vidéo",
|
||||
"api_luma_i2v": "Luma Image vers Vidéo",
|
||||
"api_hailuo_minimax_i2v": "MiniMax : Image vers Vidéo",
|
||||
"api_hailuo_minimax_t2v": "MiniMax : Texte vers Vidéo",
|
||||
"api_kling_effects": "Kling : Effets Vidéo",
|
||||
"api_kling_flf": "Kling : FLF2V",
|
||||
"api_kling_i2v": "Kling : Image vers Vidéo",
|
||||
"api_luma_i2v": "Luma : Image vers Vidéo",
|
||||
"api_luma_t2v": "Luma : Texte vers Vidéo",
|
||||
"api_moonvalley_image_to_video": "Moonvalley : Image vers Vidéo",
|
||||
"api_moonvalley_text_to_video": "Moonvalley : Texte vers Vidéo",
|
||||
"api_pika_i2v": "Pika : Image vers Vidéo",
|
||||
"api_pika_scene": "Pika Scènes : Images vers Vidéo",
|
||||
"api_pixverse_t2v": "PixVerse Texte vers Vidéo",
|
||||
"api_pixverse_template_i2v": "PixVerse Template Effects: Image vers Vidéo",
|
||||
"api_veo2_i2v": "Veo2 Image vers Vidéo"
|
||||
"api_pixverse_i2v": "PixVerse : Image vers Vidéo",
|
||||
"api_pixverse_t2v": "PixVerse : Texte vers Vidéo",
|
||||
"api_pixverse_template_i2v": "PixVerse Templates : Image vers Vidéo",
|
||||
"api_runway_first_last_frame": "Runway : Premier Dernier Frame vers Vidéo",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway : Gen3a Turbo Image vers Vidéo",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway : Gen4 Turbo Image vers Vidéo",
|
||||
"api_veo2_i2v": "Veo2 : Image vers Vidéo"
|
||||
}
|
||||
},
|
||||
"templateDescription": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Utilisez Hunyuan3D 2mv pour générer des modèles à partir de plusieurs vues.",
|
||||
"hunyuan-3d-turbo": "Utilisez Hunyuan3D 2mv turbo pour générer des modèles à partir de plusieurs vues.",
|
||||
"hunyuan3d-non-multiview-train": "Utilisez Hunyuan3D 2.0 pour générer des modèles à partir d'une seule vue.",
|
||||
"stable_zero123_example": "Générez des vues 3D à partir d'images uniques."
|
||||
"3d_hunyuan3d_image_to_model": "Générez des modèles 3D à partir d'images uniques avec Hunyuan3D 2.0.",
|
||||
"3d_hunyuan3d_multiview_to_model": "Générez des modèles 3D à partir de plusieurs vues avec Hunyuan3D 2.0 MV.",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Générez des modèles 3D à partir de plusieurs vues avec Hunyuan3D 2.0 MV Turbo.",
|
||||
"stable_zero123_example": "Générez des vues 3D à partir d'images uniques avec Stable Zero123."
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Générez des modèles 3D détaillés à partir d'une seule photo avec Rodin AI.",
|
||||
"api_rodin_multiview_to_model": "Sculptez des modèles 3D complets à partir de plusieurs vues avec Rodin.",
|
||||
"api_tripo_image_to_model": "Générez des assets 3D professionnels à partir d'images 2D avec Tripo.",
|
||||
"api_tripo_multiview_to_model": "Construisez des modèles 3D à partir de multiples angles avec le scanner avancé de Tripo.",
|
||||
"api_tripo_text_to_model": "Créez des objets 3D à partir de descriptions textuelles avec Tripo."
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Contrôlez la composition d'image avec des zones.",
|
||||
"area_composition_reversed": "Inversez le workflow de composition de zones.",
|
||||
"area_composition_square_area_for_subject": "Créez un placement cohérent du sujet."
|
||||
"area_composition": "Générez des images en contrôlant la composition avec des zones définies.",
|
||||
"area_composition_square_area_for_subject": "Générez des images avec un placement cohérent du sujet grâce à la composition de zones."
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Générez de l'audio à partir de descriptions textuelles."
|
||||
"audio_ace_step_1_m2m_editing": "Éditez des chansons existantes pour changer le style et les paroles avec ACE-Step v1 M2M.",
|
||||
"audio_ace_step_1_t2a_instrumentals": "Générez de la musique instrumentale à partir de texte avec ACE-Step v1.",
|
||||
"audio_ace_step_1_t2a_song": "Générez des chansons avec voix à partir de texte avec ACE-Step v1, supportant plusieurs langues et la personnalisation du style.",
|
||||
"audio_stable_audio_example": "Générez de l'audio à partir de descriptions textuelles avec Stable Audio."
|
||||
},
|
||||
"Basics": {
|
||||
"default": "Générez des images à partir de descriptions textuelles.",
|
||||
"embedding_example": "Utilisez l'inversion textuelle pour des styles cohérents.",
|
||||
"gligen_textbox_example": "Spécifiez l'emplacement et la taille des objets.",
|
||||
"embedding_example": "Générez des images avec inversion textuelle pour des styles cohérents.",
|
||||
"gligen_textbox_example": "Générez des images avec un placement précis des objets grâce à des zones de texte.",
|
||||
"image2image": "Transformez des images existantes à l'aide de prompts textuels.",
|
||||
"inpain_model_outpainting": "Étendez les images au-delà de leurs limites d'origine.",
|
||||
"inpaint_example": "Modifiez de façon transparente des parties spécifiques d'une image.",
|
||||
"lora": "Appliquez des modèles LoRA pour des styles ou sujets spécialisés.",
|
||||
"lora_multiple": "Combinez plusieurs modèles LoRA pour des résultats uniques."
|
||||
"inpaint_model_outpainting": "Étendez les images au-delà de leurs limites d'origine.",
|
||||
"lora": "Générez des images avec des modèles LoRA pour des styles ou sujets spécialisés.",
|
||||
"lora_multiple": "Générez des images en combinant plusieurs modèles LoRA."
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "Générez des images à partir de références de pose.",
|
||||
"controlnet_example": "Contrôlez la génération d'image avec des images de référence.",
|
||||
"depth_controlnet": "Créez des images avec une génération sensible à la profondeur.",
|
||||
"depth_t2i_adapter": "Générez rapidement des images sensibles à la profondeur avec un adaptateur T2I.",
|
||||
"mixing_controlnets": "Combinez plusieurs modèles ControlNet ensemble."
|
||||
"2_pass_pose_worship": "Générez des images guidées par des références de pose avec ControlNet.",
|
||||
"controlnet_example": "Générez des images guidées par des images de gribouillage avec ControlNet.",
|
||||
"depth_controlnet": "Générez des images guidées par la profondeur avec ControlNet.",
|
||||
"depth_t2i_adapter": "Générez des images guidées par la profondeur avec l'adaptateur T2I.",
|
||||
"mixing_controlnets": "Générez des images en combinant plusieurs modèles ControlNet."
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Générez des images à partir de la détection de contours.",
|
||||
"flux_depth_lora_example": "Créez des images avec LoRA sensible à la profondeur.",
|
||||
"flux_dev_checkpoint_example": "Créez des images avec les modèles de développement Flux.",
|
||||
"flux_fill_inpaint_example": "Complétez les parties manquantes d'une image.",
|
||||
"flux_fill_outpaint_example": "Étendez les images avec l'outpainting Flux.",
|
||||
"flux_redux_model_example": "Transférez le style d'une image de référence pour guider la génération d'image avec Flux.",
|
||||
"flux_schnell": "Générez des images rapidement avec Flux Schnell."
|
||||
"flux_canny_model_example": "Générez des images guidées par détection de contours avec Flux Canny.",
|
||||
"flux_depth_lora_example": "Générez des images guidées par la profondeur avec Flux LoRA.",
|
||||
"flux_dev_checkpoint_example": "Générez des images avec la version quantifiée fp8 de Flux Dev. Idéal pour les appareils à faible VRAM, nécessite un seul fichier modèle, mais la qualité d'image est légèrement inférieure à la version complète.",
|
||||
"flux_dev_full_text_to_image": "Générez des images de haute qualité avec la version complète de Flux Dev. Nécessite plus de VRAM et plusieurs fichiers modèles, mais offre la meilleure fidélité au prompt et la meilleure qualité d'image.",
|
||||
"flux_fill_inpaint_example": "Complétez les parties manquantes d'une image avec Flux inpainting.",
|
||||
"flux_fill_outpaint_example": "Étendez les images au-delà des limites avec Flux outpainting.",
|
||||
"flux_kontext_dev_basic": "Éditez des images avec Flux Kontext (tous les nœuds visibles), parfait pour apprendre le workflow.",
|
||||
"flux_kontext_dev_grouped": "Version simplifiée de Flux Kontext avec des nœuds groupés pour un espace de travail plus propre.",
|
||||
"flux_redux_model_example": "Générez des images en transférant le style d'images de référence avec Flux Redux.",
|
||||
"flux_schnell": "Générez rapidement des images avec la version quantifiée fp8 de Flux Schnell. Parfait pour le matériel bas de gamme, nécessite seulement 4 étapes.",
|
||||
"flux_schnell_full_text_to_image": "Générez rapidement des images avec la version complète de Flux Schnell. Licence Apache2.0, seulement 4 étapes tout en maintenant une bonne qualité."
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "Modifiez des images avec HiDream E1.",
|
||||
"hidream_i1_dev": "Générez des images avec HiDream I1 Dev.",
|
||||
"hidream_i1_fast": "Générez rapidement des images avec HiDream I1.",
|
||||
"hidream_i1_full": "Générez des images avec HiDream I1.",
|
||||
"sd3_5_large_blur": "Générez des images à partir d'images de référence floues avec SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Utilisez la détection de contours pour guider la génération d'images avec SD 3.5.",
|
||||
"sd3_5_large_depth": "Créez des images sensibles à la profondeur avec SD 3.5.",
|
||||
"hidream_e1_full": "HiDream E1 - Modèle professionnel d'édition d'image par langage naturel.",
|
||||
"hidream_i1_dev": "HiDream I1 Dev - Version équilibrée avec 28 étapes d'inférence, adaptée au matériel intermédiaire.",
|
||||
"hidream_i1_fast": "HiDream I1 Fast - Version légère avec 16 étapes, idéale pour des aperçus rapides sur du matériel bas de gamme.",
|
||||
"hidream_i1_full": "HiDream I1 Full - Version complète avec 50 étapes pour une qualité maximale.",
|
||||
"image_chroma_text_to_image": "Chroma est une version modifiée de Flux avec quelques changements d'architecture.",
|
||||
"image_cosmos_predict2_2B_t2i": "Générez des images avec Cosmos-Predict2 2B T2I, pour une génération physique précise, haute fidélité et riche en détails.",
|
||||
"image_lotus_depth_v1_1": "Exécutez Lotus Depth dans ComfyUI pour une estimation de profondeur monoculaire efficace et détaillée.",
|
||||
"image_omnigen2_image_edit": "Éditez des images avec des instructions en langage naturel grâce aux capacités avancées d'édition d'image et de texte d'OmniGen2.",
|
||||
"image_omnigen2_t2i": "Générez des images de haute qualité à partir de texte avec le modèle multimodal unifié 7B d'OmniGen2 et une architecture à double voie.",
|
||||
"sd3_5_large_blur": "Générez des images guidées par des images de référence floues avec SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Générez des images guidées par détection de contours avec SD 3.5 Canny ControlNet.",
|
||||
"sd3_5_large_depth": "Générez des images guidées par la profondeur avec SD 3.5.",
|
||||
"sd3_5_simple_example": "Générez des images avec SD 3.5.",
|
||||
"sdxl_refiner_prompt_example": "Améliorez les résultats SDXL avec des refineurs.",
|
||||
"sdxl_revision_text_prompts": "Transférez des concepts à partir d'images de référence pour guider la génération d'images avec SDXL.",
|
||||
"sdxl_revision_zero_positive": "Ajoutez des prompts textuels en plus des images de référence pour guider la génération d'images avec SDXL.",
|
||||
"sdxl_simple_example": "Créez des images de haute qualité avec SDXL.",
|
||||
"sdxl_refiner_prompt_example": "Améliorez les images SDXL avec des modèles de raffinement.",
|
||||
"sdxl_revision_text_prompts": "Générez des images en transférant des concepts d'images de référence avec SDXL Revision.",
|
||||
"sdxl_revision_zero_positive": "Générez des images en utilisant à la fois des prompts textuels et des images de référence avec SDXL Revision.",
|
||||
"sdxl_simple_example": "Générez des images de haute qualité avec SDXL.",
|
||||
"sdxlturbo_example": "Générez des images en une seule étape avec SDXL Turbo."
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Utilisez l'API Dall-E 2 pour faire de l'inpainting sur des images.",
|
||||
"api-openai-dall-e-2-t2i": "Utilisez l'API Dall-E 2 pour générer des images à partir de descriptions textuelles.",
|
||||
"api-openai-dall-e-3-t2i": "Utilisez l'API Dall-E 3 pour générer des images à partir de descriptions textuelles.",
|
||||
"api_bfl_flux_pro_t2i": "Créez des images avec FLUX.1 [pro] pour un excellent suivi des prompts, une qualité visuelle, des détails d'image et une grande diversité de sorties.",
|
||||
"api_ideogram_v3_t2i": "Générez des images avec un alignement prompt-image de haute qualité, du photoréalisme et du rendu de texte. Créez des logos professionnels, affiches promotionnelles, concepts de pages d'accueil, photographies de produits et plus. Composez facilement des compositions spatiales sophistiquées avec des arrière-plans complexes, un éclairage et des couleurs précis et nuancés, et des détails environnementaux réalistes.",
|
||||
"api_bfl_flux_1_kontext_max_image": "Éditez des images avec Flux.1 Kontext max image.",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "Entrez plusieurs images et éditez-les avec Flux.1 Kontext.",
|
||||
"api_bfl_flux_1_kontext_pro_image": "Éditez des images avec Flux.1 Kontext pro image.",
|
||||
"api_bfl_flux_pro_t2i": "Générez des images avec un excellent suivi de prompt et une qualité visuelle avec FLUX.1 Pro.",
|
||||
"api_ideogram_v3_t2i": "Générez des images de qualité professionnelle avec un excellent alignement prompt-image, du photoréalisme et du rendu de texte avec Ideogram V3.",
|
||||
"api_luma_photon_i2i": "Guidez la génération d'image en combinant images et prompt.",
|
||||
"api_luma_photon_style_ref": "Appliquez et mélangez des références de style avec un contrôle précis. Luma Photon capture l'essence de chaque image de référence, vous permettant de combiner des éléments visuels distincts tout en maintenant une qualité professionnelle.",
|
||||
"api_openai_image_1_i2i": "Utilisez l'API GPT Image 1 pour générer des images à partir d'images.",
|
||||
"api_openai_image_1_inpaint": "Utilisez l'API GPT Image 1 pour faire de l'inpainting sur des images.",
|
||||
"api_openai_image_1_multi_inputs": "Utilisez l'API GPT Image 1 avec plusieurs entrées pour générer des images.",
|
||||
"api_openai_image_1_t2i": "Utilisez l'API GPT Image 1 pour générer des images à partir de descriptions textuelles.",
|
||||
"api_recraft_image_gen_with_color_control": "Créez une palette personnalisée à réutiliser pour plusieurs images ou choisissez les couleurs pour chaque photo. Adaptez la palette de couleurs de votre marque et créez des visuels qui vous ressemblent.",
|
||||
"api_recraft_image_gen_with_style_control": "Contrôlez le style avec des exemples visuels, alignez le positionnement et affinez les objets. Stockez et partagez des styles pour une cohérence parfaite de la marque.",
|
||||
"api_recraft_vector_gen": "Passez d'un prompt textuel à une image vectorielle avec le générateur vectoriel IA de Recraft. Produisez des illustrations vectorielles de la meilleure qualité pour des logos, affiches, icônes, publicités, bannières et maquettes. Perfectionnez vos designs avec des fichiers SVG nets et de haute qualité. Créez des illustrations vectorielles de marque pour votre application ou site web en quelques secondes.",
|
||||
"api_stability_sd3_t2i": "Générez des images de haute qualité avec une excellente fidélité au prompt. Parfait pour les cas d'usage professionnels en résolution 1 mégapixel."
|
||||
"api_luma_photon_style_ref": "Générez des images en combinant des références de style avec un contrôle précis avec Luma Photon.",
|
||||
"api_openai_dall_e_2_inpaint": "Éditez des images avec inpainting avec l'API OpenAI Dall-E 2.",
|
||||
"api_openai_dall_e_2_t2i": "Générez des images à partir de texte avec l'API OpenAI Dall-E 2.",
|
||||
"api_openai_dall_e_3_t2i": "Générez des images à partir de texte avec l'API OpenAI Dall-E 3.",
|
||||
"api_openai_image_1_i2i": "Générez des images à partir d'images avec l'API OpenAI GPT Image 1.",
|
||||
"api_openai_image_1_inpaint": "Éditez des images avec inpainting avec l'API OpenAI GPT Image 1.",
|
||||
"api_openai_image_1_multi_inputs": "Générez des images à partir de plusieurs entrées avec l'API OpenAI GPT Image 1.",
|
||||
"api_openai_image_1_t2i": "Générez des images à partir de texte avec l'API OpenAI GPT Image 1.",
|
||||
"api_recraft_image_gen_with_color_control": "Générez des images avec des palettes de couleurs personnalisées et des visuels de marque avec Recraft.",
|
||||
"api_recraft_image_gen_with_style_control": "Contrôlez le style avec des exemples visuels, alignez le positionnement et affinez les objets. Stockez et partagez des styles pour une cohérence de marque.",
|
||||
"api_recraft_vector_gen": "Générez des images vectorielles de haute qualité à partir de texte avec le générateur vectoriel IA de Recraft.",
|
||||
"api_runway_reference_to_image": "Générez de nouvelles images basées sur des styles et des compositions de référence avec Runway.",
|
||||
"api_runway_text_to_image": "Générez des images de haute qualité à partir de texte avec le modèle IA de Runway.",
|
||||
"api_stability_ai_i2i": "Transformez des images avec une génération de haute qualité avec Stability AI, idéal pour l'édition professionnelle et le transfert de style.",
|
||||
"api_stability_ai_sd3_5_i2i": "Générez des images de haute qualité avec une excellente fidélité au prompt. Parfait pour un usage professionnel à 1 mégapixel.",
|
||||
"api_stability_ai_sd3_5_t2i": "Générez des images de haute qualité avec une excellente fidélité au prompt. Parfait pour un usage professionnel à 1 mégapixel.",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Générez des images de haute qualité avec une excellente fidélité au prompt. Parfait pour un usage professionnel à 1 mégapixel."
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Découvrez l'IA multimodale de Google avec les capacités de raisonnement de Gemini.",
|
||||
"api_openai_chat": "Discutez avec les modèles de langage avancés d'OpenAI pour des conversations intelligentes."
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "Utilisez des modèles d'upscaling pour améliorer la qualité d'image.",
|
||||
"hiresfix_esrgan_workflow": "Utilisez des modèles d'upscaling lors des étapes intermédiaires.",
|
||||
"hiresfix_latent_workflow": "Améliorez la qualité d'image dans l'espace latent.",
|
||||
"latent_upscale_different_prompt_model": "Upscalez et changez le prompt à chaque passage."
|
||||
"esrgan_example": "Agrandissez les images avec des modèles ESRGAN pour améliorer la qualité.",
|
||||
"hiresfix_esrgan_workflow": "Agrandissez les images avec des modèles ESRGAN lors des étapes intermédiaires.",
|
||||
"hiresfix_latent_workflow": "Agrandissez les images en améliorant la qualité dans l'espace latent.",
|
||||
"latent_upscale_different_prompt_model": "Agrandissez les images en changeant les prompts entre les passes."
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Générez des vidéos avec le modèle Hunyuan.",
|
||||
"image_to_video": "Transformez des images en vidéos animées.",
|
||||
"image_to_video_wan": "Générez rapidement des vidéos à partir d'images.",
|
||||
"ltxv_image_to_video": "Convertissez des images fixes en vidéos.",
|
||||
"ltxv_text_to_video": "Générez des vidéos à partir de descriptions textuelles.",
|
||||
"mochi_text_to_video_example": "Créez des vidéos avec le modèle Mochi.",
|
||||
"text_to_video_wan": "Générez rapidement des vidéos à partir de descriptions textuelles.",
|
||||
"txt_to_image_to_video": "Générez des images à partir de texte puis convertissez-les en vidéos.",
|
||||
"wan2_1_flf2v_720_f16": "Générez une vidéo en contrôlant la première et la dernière image.",
|
||||
"wan2_1_fun_control": "Guidez la génération vidéo avec le contrôle de pose, profondeur, contours et plus.",
|
||||
"wan2_1_fun_inp": "Créez des vidéos à partir d'images de début et de fin."
|
||||
"hunyuan_video_text_to_video": "Générez des vidéos à partir de texte avec le modèle Hunyuan.",
|
||||
"image_to_video": "Générez des vidéos à partir d'images fixes.",
|
||||
"image_to_video_wan": "Générez des vidéos à partir d'images avec Wan 2.1.",
|
||||
"ltxv_image_to_video": "Générez des vidéos à partir d'images fixes.",
|
||||
"ltxv_text_to_video": "Générez des vidéos à partir de texte.",
|
||||
"mochi_text_to_video_example": "Générez des vidéos à partir de texte avec le modèle Mochi.",
|
||||
"text_to_video_wan": "Générez des vidéos à partir de texte avec Wan 2.1.",
|
||||
"txt_to_image_to_video": "Générez des vidéos en créant d'abord des images à partir de texte.",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Générez des vidéos avec Cosmos-Predict2 2B Video2World, pour des simulations physiques précises, haute fidélité et cohérentes.",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Générez des vidéos de haute qualité avec un contrôle avancé de la caméra avec le modèle complet 14B.",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Générez des vidéos dynamiques avec des mouvements de caméra cinématographiques avec Wan 2.1 Fun Camera 1.3B.",
|
||||
"video_wan_vace_14B_ref2v": "Créez des vidéos qui correspondent au style et au contenu d'une image de référence.",
|
||||
"video_wan_vace_14B_t2v": "Transformez des descriptions textuelles en vidéos de haute qualité. Prend en charge 480p et 720p avec le modèle VACE-14B.",
|
||||
"video_wan_vace_14B_v2v": "Générez des vidéos en contrôlant des vidéos d'entrée et des images de référence avec Wan VACE.",
|
||||
"video_wan_vace_flf2v": "Générez des transitions vidéo fluides en définissant les images de début et de fin. Prend en charge les séquences d'images personnalisées.",
|
||||
"video_wan_vace_inpainting": "Éditez des régions spécifiques dans des vidéos tout en préservant le contenu environnant.",
|
||||
"video_wan_vace_outpainting": "Générez des vidéos étendues en agrandissant la taille avec Wan VACE outpainting.",
|
||||
"wan2_1_flf2v_720_f16": "Générez des vidéos en contrôlant la première et la dernière image avec Wan 2.1 FLF2V.",
|
||||
"wan2_1_fun_control": "Générez des vidéos guidées par la pose, la profondeur et les contours avec Wan 2.1 ControlNet.",
|
||||
"wan2_1_fun_inp": "Générez des vidéos à partir d'images de début et de fin avec Wan 2.1 inpainting."
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "Créez des vidéos raffinées à partir d'images et de texte, incluant l'intégration CGI et des effets photo tendance comme le hugging IA viral. Choisissez parmi une variété de styles et thèmes vidéo pour correspondre à votre vision créative.",
|
||||
"api_kling_i2v": "Créez des vidéos avec une excellente fidélité au prompt pour les actions, expressions et mouvements de caméra. Prend désormais en charge des prompts complexes avec des actions séquentielles pour vous permettre de diriger votre scène.",
|
||||
"api_hailuo_minimax_i2v": "Générez des vidéos raffinées à partir d'images et de texte avec intégration CGI avec MiniMax.",
|
||||
"api_hailuo_minimax_t2v": "Générez des vidéos de haute qualité directement à partir de texte. Découvrez les capacités avancées de MiniMax pour créer des récits visuels variés avec des effets CGI professionnels.",
|
||||
"api_kling_effects": "Générez des vidéos dynamiques en appliquant des effets visuels à des images avec Kling.",
|
||||
"api_kling_flf": "Générez des vidéos en contrôlant les premières et dernières images.",
|
||||
"api_kling_i2v": "Générez des vidéos avec une excellente fidélité au prompt pour les actions, expressions et mouvements de caméra avec Kling.",
|
||||
"api_luma_i2v": "Transformez des images statiques en animations magiques de haute qualité instantanément.",
|
||||
"api_pika_scene": "Utilisez plusieurs images comme ingrédients et générez des vidéos qui les intègrent toutes.",
|
||||
"api_pixverse_t2v": "Générez des vidéos avec une interprétation précise du prompt et des dynamiques vidéo impressionnantes.",
|
||||
"api_pixverse_template_i2v": "Transformez des images statiques en vidéos dynamiques avec mouvement et effets.",
|
||||
"api_veo2_i2v": "Utilisez l'API Google Veo2 pour générer des vidéos à partir d'images."
|
||||
"api_luma_t2v": "Générez des vidéos de haute qualité à partir de prompts simples.",
|
||||
"api_moonvalley_image_to_video": "Générez des vidéos cinématographiques 1080p à partir d'une image avec un modèle entraîné uniquement sur des données sous licence.",
|
||||
"api_moonvalley_text_to_video": "Générez des vidéos cinématographiques 1080p à partir de texte avec un modèle entraîné uniquement sur des données sous licence.",
|
||||
"api_pika_i2v": "Générez des vidéos animées fluides à partir d'images fixes avec Pika AI.",
|
||||
"api_pika_scene": "Générez des vidéos qui incorporent plusieurs images d'entrée avec Pika Scenes.",
|
||||
"api_pixverse_i2v": "Générez des vidéos dynamiques à partir d'images fixes avec mouvement et effets avec PixVerse.",
|
||||
"api_pixverse_t2v": "Générez des vidéos avec une interprétation précise du prompt et une dynamique visuelle impressionnante.",
|
||||
"api_pixverse_template_i2v": "Générez des vidéos dynamiques à partir d'images fixes avec mouvement et effets avec PixVerse.",
|
||||
"api_runway_first_last_frame": "Générez des transitions vidéo fluides entre deux images clés avec la précision de Runway.",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Générez des vidéos cinématographiques à partir d'images fixes avec Runway Gen3a Turbo.",
|
||||
"api_runway_gen4_turo_image_to_video": "Générez des vidéos dynamiques à partir d'images avec Runway Gen4 Turbo.",
|
||||
"api_veo2_i2v": "Générez des vidéos à partir d'images avec l'API Google Veo2."
|
||||
}
|
||||
},
|
||||
"title": "Commencez avec un modèle"
|
||||
|
||||
@@ -326,7 +326,8 @@
|
||||
"Bottom": "Bas",
|
||||
"Disabled": "Désactivé",
|
||||
"Top": "Haut"
|
||||
}
|
||||
},
|
||||
"tooltip": "Position de la barre de menu. Sur les appareils mobiles, le menu est toujours affiché en haut."
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "Valider les définitions de nœuds (lent)",
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
"noWorkflowsFound": "ワークフローが見つかりません。",
|
||||
"nodes": "ノード",
|
||||
"nodesRunning": "ノードが実行中",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "新しい問題を開く",
|
||||
"overwrite": "上書き",
|
||||
@@ -1200,81 +1201,113 @@
|
||||
"Flux": "Flux",
|
||||
"Image": "画像",
|
||||
"Image API": "画像API",
|
||||
"LLM API": "LLM API",
|
||||
"Upscaling": "アップスケーリング",
|
||||
"Video": "ビデオ",
|
||||
"Video API": "動画API"
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D マルチビュー",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D ターボ",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3Dマルチビュー",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3Dターボ",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin:画像からモデルへ",
|
||||
"api_rodin_multiview_to_model": "Rodin:マルチビューからモデルへ",
|
||||
"api_tripo_image_to_model": "Tripo:画像からモデルへ",
|
||||
"api_tripo_multiview_to_model": "Tripo:マルチビューからモデルへ",
|
||||
"api_tripo_text_to_model": "Tripo:テキストからモデルへ"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "エリア構成",
|
||||
"area_composition_reversed": "エリア構成反転",
|
||||
"area_composition_square_area_for_subject": "主題のためのエリア構成スクエア"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Stable Audio"
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 M2M編集",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1テキストからインスト音楽",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1テキストから歌",
|
||||
"audio_stable_audio_example": "Stable Audio"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "画像生成",
|
||||
"embedding_example": "埋め込み",
|
||||
"gligen_textbox_example": "Gligenテキストボックス",
|
||||
"image2image": "画像から画像へ",
|
||||
"inpain_model_outpainting": "InpaintモデルのOutpainting",
|
||||
"inpaint_example": "Inpaint",
|
||||
"lora": "Lora",
|
||||
"lora_multiple": "Lora複数"
|
||||
"inpaint_model_outpainting": "Outpainting",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "LoRA複数"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "2 Pass Pose Worship",
|
||||
"controlnet_example": "ControlNet",
|
||||
"controlnet_example": "Scribble ControlNet",
|
||||
"depth_controlnet": "Depth ControlNet",
|
||||
"depth_t2i_adapter": "Depth T2Iアダプタ",
|
||||
"mixing_controlnets": "ControlNetsの混合"
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Flux Cannyモデル",
|
||||
"flux_depth_lora_example": "Flux Depth Lora",
|
||||
"flux_dev_checkpoint_example": "Flux Dev",
|
||||
"flux_depth_lora_example": "Flux Depth LoRA",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8",
|
||||
"flux_dev_full_text_to_image": "Flux Devフルテキストから画像へ",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev(ベーシック)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev(グループ化)",
|
||||
"flux_redux_model_example": "Flux Reduxモデル",
|
||||
"flux_schnell": "Flux Schnell"
|
||||
"flux_schnell": "Flux Schnell fp8",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnellフルテキストから画像へ"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 Full",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full",
|
||||
"sd3_5_large_blur": "SD3.5 ラージブラー",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 ラージキャニーコントロールネット",
|
||||
"sd3_5_large_depth": "SD3.5 ラージデプス",
|
||||
"sd3_5_simple_example": "SD3.5 シンプル",
|
||||
"sdxl_refiner_prompt_example": "SDXL Refinerプロンプト",
|
||||
"sdxl_revision_text_prompts": "SDXL Revisionテキストプロンプト",
|
||||
"sdxl_revision_zero_positive": "SDXL Revisionゼロポジティブ",
|
||||
"image_chroma_text_to_image": "Chromaテキストから画像へ",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B T2I",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth",
|
||||
"image_omnigen2_image_edit": "OmniGen2画像編集",
|
||||
"image_omnigen2_t2i": "OmniGen2テキストから画像へ",
|
||||
"sd3_5_large_blur": "SD3.5ラージブラー",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5ラージキャニーコントロールネット",
|
||||
"sd3_5_large_depth": "SD3.5ラージデプス",
|
||||
"sd3_5_simple_example": "SD3.5シンプル",
|
||||
"sdxl_refiner_prompt_example": "SDXLリファイナープロンプト",
|
||||
"sdxl_revision_text_prompts": "SDXLリビジョンテキストプロンプト",
|
||||
"sdxl_revision_zero_positive": "SDXLリビジョンゼロポジティブ",
|
||||
"sdxl_simple_example": "SDXLシンプル",
|
||||
"sdxlturbo_example": "SDXLターボ"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 インペイント",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 テキストから画像へ",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 テキストから画像へ",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra テキストから画像へ",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 テキストから画像へ",
|
||||
"api_luma_photon_i2i": "Luma Photon 画像から画像へ",
|
||||
"api_luma_photon_style_ref": "Luma Photon スタイル参照",
|
||||
"api_openai_image_1_i2i": "OpenAI Image-1 画像から画像へ",
|
||||
"api_openai_image_1_inpaint": "OpenAI Image-1 インペイント",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI Image-1 複数入力",
|
||||
"api_openai_image_1_t2i": "OpenAI Image-1 テキストから画像へ",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft カラーコントロール画像生成",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft スタイルコントロール画像生成",
|
||||
"api_recraft_vector_gen": "Recraft ベクター生成",
|
||||
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra テキストから画像へ"
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext Max",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext複数画像入力",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext Pro",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]:テキストから画像へ",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3:テキストから画像へ",
|
||||
"api_luma_photon_i2i": "Luma Photon:画像から画像へ",
|
||||
"api_luma_photon_style_ref": "Luma Photon:スタイル参照",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI: Dall-E 2インペイント",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI: Dall-E 2テキストから画像へ",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI: Dall-E 3テキストから画像へ",
|
||||
"api_openai_image_1_i2i": "OpenAI: GPT-Image-1画像から画像へ",
|
||||
"api_openai_image_1_inpaint": "OpenAI: GPT-Image-1インペイント",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI: GPT-Image-1複数入力",
|
||||
"api_openai_image_1_t2i": "OpenAI: GPT-Image-1テキストから画像へ",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft:カラーコントロール画像生成",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft:スタイルコントロール画像生成",
|
||||
"api_recraft_vector_gen": "Recraft:ベクター生成",
|
||||
"api_runway_reference_to_image": "Runway:リファレンスから画像へ",
|
||||
"api_runway_text_to_image": "Runway:テキストから画像へ",
|
||||
"api_stability_ai_i2i": "Stability AI:画像から画像へ",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI:SD3.5画像から画像へ",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI:SD3.5テキストから画像へ",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI:Stable Image Ultraテキストから画像へ"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google Gemini:チャット",
|
||||
"api_openai_chat": "OpenAI:チャット"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "ESRGAN",
|
||||
@@ -1284,125 +1317,196 @@
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Hunyuanビデオテキストからビデオへ",
|
||||
"image_to_video": "画像からビデオへ",
|
||||
"image_to_video_wan": "Wan 2.1 画像からビデオへ",
|
||||
"image_to_video": "SVD画像からビデオへ",
|
||||
"image_to_video_wan": "Wan 2.1画像からビデオへ",
|
||||
"ltxv_image_to_video": "LTXV画像からビデオへ",
|
||||
"ltxv_text_to_video": "LTXVテキストからビデオへ",
|
||||
"mochi_text_to_video_example": "Mochiテキストからビデオへ",
|
||||
"text_to_video_wan": "Wan 2.1 テキストからビデオへ",
|
||||
"txt_to_image_to_video": "テキストから画像へ、画像からビデオへ",
|
||||
"text_to_video_wan": "Wan 2.1テキストからビデオへ",
|
||||
"txt_to_image_to_video": "SVDテキストから画像へ、画像からビデオへ",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACEリファレンスからビデオへ",
|
||||
"video_wan_vace_14B_t2v": "Wan VACEテキストからビデオへ",
|
||||
"video_wan_vace_14B_v2v": "Wan VACEコントロールビデオ",
|
||||
"video_wan_vace_flf2v": "Wan VACEファースト-ラストフレーム",
|
||||
"video_wan_vace_inpainting": "Wan VACEインペインティング",
|
||||
"video_wan_vace_outpainting": "Wan VACEアウトペインティング",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet",
|
||||
"wan2_1_fun_inp": "Wan 2.1 インペインティング"
|
||||
"wan2_1_fun_inp": "Wan 2.1インペインティング"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "MiniMax 画像から動画へ",
|
||||
"api_kling_i2v": "Kling 画像から動画へ",
|
||||
"api_luma_i2v": "Luma 画像から動画へ",
|
||||
"api_pika_scene": "Pika シーン: 画像から動画へ",
|
||||
"api_pixverse_t2v": "PixVerse テキストから動画へ",
|
||||
"api_pixverse_template_i2v": "PixVerse Template Effects: 画像から動画へ",
|
||||
"api_veo2_i2v": "Veo2 画像から動画へ"
|
||||
"api_hailuo_minimax_i2v": "MiniMax:画像から動画へ",
|
||||
"api_hailuo_minimax_t2v": "MiniMax:テキストから動画へ",
|
||||
"api_kling_effects": "Kling:ビデオエフェクト",
|
||||
"api_kling_flf": "Kling:FLF2V",
|
||||
"api_kling_i2v": "Kling:画像から動画へ",
|
||||
"api_luma_i2v": "Luma:画像から動画へ",
|
||||
"api_luma_t2v": "Luma:テキストから動画へ",
|
||||
"api_moonvalley_image_to_video": "Moonvalley:画像からビデオへ",
|
||||
"api_moonvalley_text_to_video": "Moonvalley:テキストからビデオへ",
|
||||
"api_pika_i2v": "Pika:画像から動画へ",
|
||||
"api_pika_scene": "Pikaシーン:画像から動画へ",
|
||||
"api_pixverse_i2v": "PixVerse:画像から動画へ",
|
||||
"api_pixverse_t2v": "PixVerse:テキストから動画へ",
|
||||
"api_pixverse_template_i2v": "PixVerse Templates:画像から動画へ",
|
||||
"api_runway_first_last_frame": "Runway:ファーストラストフレームから動画へ",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway:Gen3a Turbo画像から動画へ",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway:Gen4 Turbo画像から動画へ",
|
||||
"api_veo2_i2v": "Veo2:画像から動画へ"
|
||||
}
|
||||
},
|
||||
"templateDescription": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D 2mvで複数ビューからモデルを生成します。",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D 2mv turboで複数ビューからモデルを生成します。",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D 2.0で単一ビューからモデルを生成します。",
|
||||
"stable_zero123_example": "単一画像から3Dビューを生成します。"
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0で単一画像から3Dモデルを生成します。",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D 2.0 MVで複数ビューから3Dモデルを生成します。",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D 2.0 MV Turboで複数ビューから3Dモデルを生成します。",
|
||||
"stable_zero123_example": "Stable Zero123で単一画像から3Dビューを生成します。"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin AIで1枚の写真から高精細3Dモデルを生成します。",
|
||||
"api_rodin_multiview_to_model": "Rodinで複数視点から3Dモデルを再構築します。",
|
||||
"api_tripo_image_to_model": "Tripoエンジンで2D画像からプロ品質3Dアセットを生成します。",
|
||||
"api_tripo_multiview_to_model": "Tripoの高度なスキャナーで複数角度から3Dモデルを構築します。",
|
||||
"api_tripo_text_to_model": "Tripoでテキスト説明から3Dオブジェクトを作成します。"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "エリアで画像構成をコントロールします。",
|
||||
"area_composition_reversed": "エリア構成ワークフローを逆転します。",
|
||||
"area_composition_square_area_for_subject": "被写体の配置を一貫させます。"
|
||||
"area_composition_square_area_for_subject": "エリア構成で被写体の配置を一貫させます。"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "テキストの説明から音声を生成します。"
|
||||
"audio_ace_step_1_m2m_editing": "ACE-Step v1 M2Mで既存楽曲のスタイルや歌詞を編集します。",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1でテキストからインスト音楽を生成します。",
|
||||
"audio_ace_step_1_t2a_song": "ACE-Step v1でテキストからボーカル付き楽曲を生成。多言語・スタイルカスタマイズ対応。",
|
||||
"audio_stable_audio_example": "テキスト説明から音声を生成します。"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "テキストの説明から画像を生成します。",
|
||||
"embedding_example": "テキスト反転を使って一貫したスタイルを実現します。",
|
||||
"gligen_textbox_example": "オブジェクトの位置とサイズを指定します。",
|
||||
"default": "テキスト説明から画像を生成します。",
|
||||
"embedding_example": "テキスト反転で一貫したスタイルの画像を生成します。",
|
||||
"gligen_textbox_example": "テキストボックスでオブジェクトの位置を正確に指定して画像を生成します。",
|
||||
"image2image": "テキストプロンプトを使って既存の画像を変換します。",
|
||||
"inpain_model_outpainting": "画像を元の境界を超えて拡張します。",
|
||||
"inpaint_example": "画像の特定部分をシームレスに編集します。",
|
||||
"lora": "LoRAモデルを適用して特定のスタイルや対象を表現します。",
|
||||
"lora_multiple": "複数のLoRAモデルを組み合わせて独自の結果を得ます。"
|
||||
"inpaint_model_outpainting": "画像を元の境界を超えて拡張します。",
|
||||
"lora": "LoRAモデルで特定のスタイルやテーマの画像を生成します。",
|
||||
"lora_multiple": "複数のLoRAモデルを組み合わせて画像を生成します。"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "ポーズ参照から画像を生成します。",
|
||||
"controlnet_example": "参照画像で画像生成をコントロールします。",
|
||||
"depth_controlnet": "深度認識生成で画像を作成します。",
|
||||
"2_pass_pose_worship": "ポーズ参照で画像を生成します。",
|
||||
"controlnet_example": "落書き参照画像で画像生成をガイドします。",
|
||||
"depth_controlnet": "深度情報で画像生成をガイドします。",
|
||||
"depth_t2i_adapter": "T2Iアダプターで素早く深度認識画像を生成します。",
|
||||
"mixing_controlnets": "複数のControlNetモデルを組み合わせます。"
|
||||
"mixing_controlnets": "複数のControlNetモデルを組み合わせて画像を生成します。"
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "エッジ検出から画像を生成します。",
|
||||
"flux_depth_lora_example": "深度認識LoRAで画像を生成します。",
|
||||
"flux_dev_checkpoint_example": "Flux開発モデルを使って画像を生成します。",
|
||||
"flux_fill_inpaint_example": "画像の欠損部分を補完します。",
|
||||
"flux_fill_outpaint_example": "Fluxのアウトペイントで画像を拡張します。",
|
||||
"flux_redux_model_example": "参照画像のスタイルを転送し、Fluxで画像生成をガイドします。",
|
||||
"flux_schnell": "Flux Schnellで素早く画像を生成します。"
|
||||
"flux_canny_model_example": "エッジ検出でFlux画像生成をガイドします。",
|
||||
"flux_depth_lora_example": "深度情報でFlux LoRA画像生成をガイドします。",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8量子化版で画像生成。低VRAMデバイス向け、モデルファイル1つでOK、画質はフル版よりやや低め。",
|
||||
"flux_dev_full_text_to_image": "Flux Devフル版で高品質画像生成。大きなVRAMと複数モデルファイルが必要ですが、プロンプト追従性・画質ともに最高です。",
|
||||
"flux_fill_inpaint_example": "Fluxで画像の欠損部分を補完します。",
|
||||
"flux_fill_outpaint_example": "Fluxで画像を元の枠を超えて拡張します。",
|
||||
"flux_kontext_dev_basic": "Flux Kontext(全ノード表示)で画像を編集。ワークフロー学習に最適です。",
|
||||
"flux_kontext_dev_grouped": "ノードをグループ化したFlux Kontext。作業スペースがすっきりします。",
|
||||
"flux_redux_model_example": "参照画像のスタイルを転送し、Flux Reduxで新しい画像を生成します。",
|
||||
"flux_schnell": "Flux Schnell fp8量子化版で高速画像生成。低スペック向け、4ステップで完了。",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnellフル版で高速かつ高品質な画像生成。Apache2.0ライセンス、4ステップで完了。"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1で画像を編集します。",
|
||||
"hidream_i1_dev": "HiDream I1 Devで画像を生成します。",
|
||||
"hidream_i1_fast": "HiDream I1で素早く画像を生成します。",
|
||||
"hidream_i1_full": "HiDream I1で画像を生成します。",
|
||||
"sd3_5_large_blur": "SD 3.5でぼかし参照画像から画像を生成します。",
|
||||
"sd3_5_large_canny_controlnet_example": "SD 3.5でエッジ検出を使って画像生成をガイドします。",
|
||||
"sd3_5_large_depth": "SD 3.5で深度認識画像を生成します。",
|
||||
"hidream_e1_full": "HiDream E1 - プロ向け自然言語画像編集モデル。",
|
||||
"hidream_i1_dev": "HiDream I1 Dev - 28ステップ推論のバランス型。中程度のハードウェア向け。",
|
||||
"hidream_i1_fast": "HiDream I1 Fast - 16ステップの軽量版。低スペックでも高速プレビュー可能。",
|
||||
"hidream_i1_full": "HiDream I1 Full - 50ステップのフル版。最高画質。",
|
||||
"image_chroma_text_to_image": "ChromaはFluxをベースに構造を一部変更したモデルです。",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos-Predict2 2B T2Iで物理的に正確で高精細・高忠実度な画像を生成します。",
|
||||
"image_lotus_depth_v1_1": "ComfyUIでLotus Depthを実行し、高精細な単眼深度推定を実現します。",
|
||||
"image_omnigen2_image_edit": "OmniGen2の高度な画像編集・テキスト描画機能で自然言語指示による画像編集が可能です。",
|
||||
"image_omnigen2_t2i": "OmniGen2の統合7Bマルチモーダルモデルとデュアルパス構造で高品質な画像をテキストから生成します。",
|
||||
"sd3_5_large_blur": "SD 3.5でぼかし参照画像を使って画像生成をガイドします。",
|
||||
"sd3_5_large_canny_controlnet_example": "SD 3.5 Canny ControlNetでエッジ検出を使って画像生成をガイドします。",
|
||||
"sd3_5_large_depth": "SD 3.5で深度情報を使って画像生成をガイドします。",
|
||||
"sd3_5_simple_example": "SD 3.5で画像を生成します。",
|
||||
"sdxl_refiner_prompt_example": "SDXLの出力をリファイナーで強化します。",
|
||||
"sdxl_revision_text_prompts": "参照画像からコンセプトを転送し、SDXLで画像生成をガイドします。",
|
||||
"sdxl_revision_zero_positive": "参照画像とテキストプロンプトを組み合わせてSDXLで画像生成をガイドします。",
|
||||
"sdxl_refiner_prompt_example": "RefinerモデルでSDXL画像を強化します。",
|
||||
"sdxl_revision_text_prompts": "参照画像のコンセプトを転送し、SDXL Revisionで画像生成をガイドします。",
|
||||
"sdxl_revision_zero_positive": "テキストプロンプトと参照画像を組み合わせてSDXL Revisionで画像生成します。",
|
||||
"sdxl_simple_example": "SDXLで高品質な画像を生成します。",
|
||||
"sdxlturbo_example": "SDXL Turboでワンステップで画像を生成します。"
|
||||
"sdxlturbo_example": "SDXL Turboでワンステップ画像生成。"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 APIで画像のインペイントを行います。",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 APIでテキストの説明から画像を生成します。",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 APIでテキストの説明から画像を生成します。",
|
||||
"api_bfl_flux_pro_t2i": "FLUX.1 [pro]で優れたプロンプト追従性、画質、ディテール、多様な出力の画像を生成します。",
|
||||
"api_ideogram_v3_t2i": "高品質な画像・プロンプト整合性、フォトリアリズム、テキスト描画で画像を生成します。プロ品質のロゴ、ポスター、ランディングページ、商品写真などを作成。複雑な背景や精密なライティング、リアルな環境ディテールで洗練された空間構成を簡単に作成できます。",
|
||||
"api_bfl_flux_1_kontext_max_image": "Flux.1 Kontext max imageで画像を編集します。",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "複数画像を入力し、Flux.1 Kontextで編集します。",
|
||||
"api_bfl_flux_1_kontext_pro_image": "Flux.1 Kontext pro imageで画像を編集します。",
|
||||
"api_bfl_flux_pro_t2i": "FLUX.1 Proで優れたプロンプト追従性と画質の画像を生成します。",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3でプロンプト整合性・フォトリアリズム・テキスト描画に優れたプロ品質画像を生成します。",
|
||||
"api_luma_photon_i2i": "画像とプロンプトを組み合わせて画像生成をガイドします。",
|
||||
"api_luma_photon_style_ref": "スタイル参照を正確に適用・ブレンドします。Luma Photonは各参照画像の本質を捉え、異なるビジュアル要素を組み合わせつつプロ品質を維持します。",
|
||||
"api_openai_image_1_i2i": "GPT Image 1 APIで画像から画像を生成します。",
|
||||
"api_openai_image_1_inpaint": "GPT Image 1 APIで画像のインペイントを行います。",
|
||||
"api_openai_image_1_multi_inputs": "GPT Image 1 APIで複数入力を使って画像を生成します。",
|
||||
"api_openai_image_1_t2i": "GPT Image 1 APIでテキストの説明から画像を生成します。",
|
||||
"api_recraft_image_gen_with_color_control": "カスタムパレットを作成して複数画像で再利用したり、各写真の色を手動で選択できます。ブランドのカラーパレットに合わせて独自のビジュアルを作成します。",
|
||||
"api_recraft_image_gen_with_style_control": "ビジュアル例でスタイルを制御し、位置合わせやオブジェクトの微調整が可能です。スタイルを保存・共有してブランドの一貫性を保ちます。",
|
||||
"api_recraft_vector_gen": "テキストプロンプトからRecraftのAIベクター生成でベクター画像を作成します。ロゴ、ポスター、アイコンセット、広告、バナー、モックアップに最適な高品質SVGファイルを生成。アプリやウェブサイト用のブランドベクターイラストを数秒で作成します。",
|
||||
"api_stability_sd3_t2i": "高品質でプロンプト追従性の高い画像を生成します。1メガピクセル解像度でプロ用途に最適です。"
|
||||
"api_luma_photon_style_ref": "Luma Photonでスタイル参照をブレンドし、正確にコントロールします。",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI Dall-E 2 APIでインペイントを行います。",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI Dall-E 2 APIでテキストから画像を生成します。",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI Dall-E 3 APIでテキストから画像を生成します。",
|
||||
"api_openai_image_1_i2i": "OpenAI GPT Image 1 APIで画像から画像を生成します。",
|
||||
"api_openai_image_1_inpaint": "OpenAI GPT Image 1 APIでインペイントを行います。",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI GPT Image 1 APIで複数入力から画像を生成します。",
|
||||
"api_openai_image_1_t2i": "OpenAI GPT Image 1 APIでテキストから画像を生成します。",
|
||||
"api_recraft_image_gen_with_color_control": "Recraftでカスタムカラーパレットやブランドビジュアルの画像を生成します。",
|
||||
"api_recraft_image_gen_with_style_control": "ビジュアル例でスタイル・位置合わせ・オブジェクト微調整を制御。スタイル保存・共有でブランド一貫性を実現。",
|
||||
"api_recraft_vector_gen": "RecraftのAIベクター生成でテキストから高品質ベクター画像を生成します。",
|
||||
"api_runway_reference_to_image": "RunwayのAIで参照スタイル・構図から新しい画像を生成します。",
|
||||
"api_runway_text_to_image": "RunwayのAIモデルでテキストから高品質画像を生成します。",
|
||||
"api_stability_ai_i2i": "Stability AIで高品質な画像変換。プロ編集やスタイル転送に最適。",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI SD3.5で高品質・高プロンプト追従性の画像変換。1メガピクセル対応。",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI SD3.5で高品質・高プロンプト追従性の画像を生成。1メガピクセル対応。",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "高品質・高プロンプト追従性の画像を生成。1メガピクセルでプロ用途に最適。"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google GeminiのマルチモーダルAIと推論能力を体験できます。",
|
||||
"api_openai_chat": "OpenAIの高度な言語モデルとインテリジェントな会話を楽しめます。"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "アップスケールモデルで画像品質を向上させます。",
|
||||
"hiresfix_esrgan_workflow": "中間ステップでアップスケールモデルを使用します。",
|
||||
"esrgan_example": "ESRGANモデルで画像をアップスケールします。",
|
||||
"hiresfix_esrgan_workflow": "中間ステップでESRGANモデルを使って画像をアップスケールします。",
|
||||
"hiresfix_latent_workflow": "latent空間で画像品質を向上させます。",
|
||||
"latent_upscale_different_prompt_model": "アップスケールしつつパスごとにプロンプトを変更します。"
|
||||
"latent_upscale_different_prompt_model": "アップスケールしつつ各パスでプロンプトを変更します。"
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Hunyuanモデルで動画を生成します。",
|
||||
"image_to_video": "画像をアニメーション動画に変換します。",
|
||||
"image_to_video_wan": "画像から素早く動画を生成します。",
|
||||
"ltxv_image_to_video": "静止画像を動画に変換します。",
|
||||
"ltxv_text_to_video": "テキストの説明から動画を生成します。",
|
||||
"mochi_text_to_video_example": "Mochiモデルで動画を作成します。",
|
||||
"text_to_video_wan": "テキストの説明から素早く動画を生成します。",
|
||||
"txt_to_image_to_video": "テキストから画像を生成し、それを動画に変換します。",
|
||||
"wan2_1_flf2v_720_f16": "最初と最後のフレームを制御して動画を生成します。",
|
||||
"wan2_1_fun_control": "ポーズ、深度、エッジ制御などで動画生成をガイドします。",
|
||||
"wan2_1_fun_inp": "開始フレームと終了フレームから動画を作成します。"
|
||||
"hunyuan_video_text_to_video": "Hunyuanモデルでテキストから動画を生成します。",
|
||||
"image_to_video": "静止画像から動画を生成します。",
|
||||
"image_to_video_wan": "Wan 2.1で画像から動画を生成します。",
|
||||
"ltxv_image_to_video": "静止画像から動画を生成します。",
|
||||
"ltxv_text_to_video": "テキストから動画を生成します。",
|
||||
"mochi_text_to_video_example": "Mochiモデルでテキストから動画を生成します。",
|
||||
"text_to_video_wan": "Wan 2.1でテキストから動画を生成します。",
|
||||
"txt_to_image_to_video": "まず画像を生成し、それを動画に変換します。",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos-Predict2 2B Video2Worldで物理的に正確・高忠実度・一貫性のある動画を生成します。",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14Bで高度なカメラ制御の高品質動画を生成します。",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3Bで映画的なカメラワークの動画を生成します。",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACEで参照画像のスタイル・内容に合った動画を生成します。",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE-14Bでテキストから高品質な動画を生成。480p/720p対応。",
|
||||
"video_wan_vace_14B_v2v": "Wan VACEで入力動画や参照画像をコントロールして新しい動画を生成します。",
|
||||
"video_wan_vace_flf2v": "Wan VACEで開始・終了フレームを指定し、滑らかな動画遷移を生成します。",
|
||||
"video_wan_vace_inpainting": "Wan VACEで動画の特定領域を編集し、周囲の内容を保持します。",
|
||||
"video_wan_vace_outpainting": "Wan VACEのアウトペイントで動画サイズを拡張します。",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2Vで最初と最後のフレームを制御して720p動画を生成します。",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNetでポーズ・深度・エッジ制御による動画生成。",
|
||||
"wan2_1_fun_inp": "Wan 2.1で開始・終了フレームから動画を生成します。"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "画像とテキストから洗練された動画を作成。CGI統合や流行のAIハグなどのエフェクトも。多彩なスタイルやテーマから選択可能です。",
|
||||
"api_kling_i2v": "アクション、表情、カメラワークのプロンプト追従性に優れた動画を作成します。複雑なシーケンシャルアクションもサポートし、あなたがシーンの監督になれます。",
|
||||
"api_luma_i2v": "静止画像から瞬時に高品質なアニメーションを作成します。",
|
||||
"api_pika_scene": "複数の画像を素材として使い、それらを組み込んだ動画を生成します。",
|
||||
"api_pixverse_t2v": "プロンプト解釈が正確で、ダイナミックな動画を生成します。",
|
||||
"api_pixverse_template_i2v": "静止画像を動きやエフェクトのあるダイナミックな動画に変換します。",
|
||||
"api_hailuo_minimax_i2v": "MiniMaxで画像+テキストからCGI統合の洗練動画を生成します。",
|
||||
"api_hailuo_minimax_t2v": "MiniMaxでテキストから高品質動画を直接生成。CGI効果や多彩なスタイルに対応。",
|
||||
"api_kling_effects": "Klingで画像にビジュアルエフェクトを適用し、ダイナミックな動画を生成します。",
|
||||
"api_kling_flf": "Klingで最初と最後のフレームを制御して動画を生成します。",
|
||||
"api_kling_i2v": "Klingでアクション・表情・カメラワークのプロンプト追従性に優れた動画を生成します。",
|
||||
"api_luma_i2v": "Lumaで静止画像から高品質アニメーションを瞬時に生成します。",
|
||||
"api_luma_t2v": "Lumaでシンプルなプロンプトから高品質動画を生成します。",
|
||||
"api_moonvalley_image_to_video": "Moonvalleyで画像から1080p映画品質動画を生成。学習データは全てライセンス済み。",
|
||||
"api_moonvalley_text_to_video": "Moonvalleyでテキストから1080p映画品質動画を生成。学習データは全てライセンス済み。",
|
||||
"api_pika_i2v": "Pika AIで静止画像から滑らかなアニメ動画を生成します。",
|
||||
"api_pika_scene": "Pika Scenesで複数画像を組み込んだ動画を生成します。",
|
||||
"api_pixverse_i2v": "PixVerseで静止画像から動きやエフェクトのあるダイナミック動画を生成します。",
|
||||
"api_pixverse_t2v": "PixVerseでプロンプト解釈が正確でダイナミックな動画を生成します。",
|
||||
"api_pixverse_template_i2v": "PixVerseで静止画像から動きやエフェクトのあるダイナミック動画を生成します。",
|
||||
"api_runway_first_last_frame": "Runwayで2つのキーフレーム間を滑らかに遷移する動画を生成します。",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway Gen3a Turboで静止画像から映画品質動画を生成します。",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway Gen4 Turboで画像からダイナミックな動画を生成します。",
|
||||
"api_veo2_i2v": "Google Veo2 APIで画像から動画を生成します。"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -326,7 +326,8 @@
|
||||
"Bottom": "下",
|
||||
"Disabled": "無効",
|
||||
"Top": "上"
|
||||
}
|
||||
},
|
||||
"tooltip": "メニューバーの位置。モバイルデバイスでは、メニューは常に上部に表示されます。"
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "ノード定義を検証(遅い)",
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
|
||||
"noWorkflowsFound": "워크플로를 찾을 수 없습니다.",
|
||||
"nodes": "노드",
|
||||
"nodesRunning": "노드 실행 중",
|
||||
"ok": "확인",
|
||||
"openNewIssue": "새 문제 열기",
|
||||
"overwrite": "덮어쓰기",
|
||||
@@ -1200,209 +1201,312 @@
|
||||
"Flux": "FLUX",
|
||||
"Image": "이미지",
|
||||
"Image API": "이미지 API",
|
||||
"LLM API": "LLM API",
|
||||
"Upscaling": "업스케일링",
|
||||
"Video": "비디오",
|
||||
"Video API": "비디오 API"
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D 다중뷰",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D 터보",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"stable_zero123_example": "스테이블 제로123"
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D 2.0 MV",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D 2.0 MV 터보",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin: 이미지 투 모델",
|
||||
"api_rodin_multiview_to_model": "Rodin: 다중뷰 투 모델",
|
||||
"api_tripo_image_to_model": "Tripo: 이미지 투 모델",
|
||||
"api_tripo_multiview_to_model": "Tripo: 다중뷰 투 모델",
|
||||
"api_tripo_text_to_model": "Tripo: 텍스트 투 모델"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "영역 구성",
|
||||
"area_composition_reversed": "역 영역 구성",
|
||||
"area_composition_square_area_for_subject": "주제를 위한 사각형 영역 구성"
|
||||
"area_composition_square_area_for_subject": "주제용 사각형 영역 구성"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "스테이블 오디오"
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 M2M 편집",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1 텍스트 투 연주곡",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1 텍스트 투 노래",
|
||||
"audio_stable_audio_example": "Stable Audio"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "이미지 생성",
|
||||
"embedding_example": "임베딩 예제",
|
||||
"gligen_textbox_example": "글리젠 텍스트박스 예제",
|
||||
"image2image": "이미지로 이미지 생성 예제",
|
||||
"inpain_model_outpainting": "인페인트 모델 아웃페인팅",
|
||||
"embedding_example": "임베딩",
|
||||
"gligen_textbox_example": "글리젠 텍스트박스",
|
||||
"image2image": "이미지 투 이미지",
|
||||
"inpaint_example": "인페인트",
|
||||
"lora": "로라",
|
||||
"lora_multiple": "다중 로라"
|
||||
"inpaint_model_outpainting": "아웃페인팅",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "다중 LoRA"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "2 패스 경배 포즈",
|
||||
"controlnet_example": "컨트롤넷",
|
||||
"2_pass_pose_worship": "포즈 컨트롤넷 2패스",
|
||||
"controlnet_example": "스크리블 컨트롤넷",
|
||||
"depth_controlnet": "깊이 컨트롤넷",
|
||||
"depth_t2i_adapter": "깊이 T2I 어댑터",
|
||||
"mixing_controlnets": "컨트롤넷 섞기"
|
||||
"mixing_controlnets": "컨트롤넷 혼합"
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "FLUX 캐니 모델 예제",
|
||||
"flux_depth_lora_example": "FLUX 깊이 로라 예제",
|
||||
"flux_dev_checkpoint_example": "FLUX Dev 예제",
|
||||
"flux_fill_inpaint_example": "FLUX 인페인트 예제",
|
||||
"flux_fill_outpaint_example": "FLUX 아웃페인트 예제",
|
||||
"flux_redux_model_example": "FLUX Redux 모델 예제",
|
||||
"flux_schnell": "FLUX Schnell"
|
||||
"flux_canny_model_example": "Flux 캐니 모델",
|
||||
"flux_depth_lora_example": "Flux 깊이 로라",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8",
|
||||
"flux_dev_full_text_to_image": "Flux Dev 전체 텍스트 투 이미지",
|
||||
"flux_fill_inpaint_example": "Flux 인페인트",
|
||||
"flux_fill_outpaint_example": "Flux 아웃페인트",
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev(기본)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev(그룹화)",
|
||||
"flux_redux_model_example": "Flux Redux 모델",
|
||||
"flux_schnell": "Flux Schnell fp8",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell 전체 텍스트 투 이미지"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 Full",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full",
|
||||
"sd3_5_large_blur": "SD3.5 Large 블러 컨트롤넷",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Large 캐니 컨트롤넷",
|
||||
"sd3_5_large_depth": "SD3.5 Large 깊이 컨트롤넷",
|
||||
"sd3_5_simple_example": "간단한 SD3.5 예제",
|
||||
"image_chroma_text_to_image": "Chroma 텍스트 투 이미지",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B T2I",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth",
|
||||
"image_omnigen2_image_edit": "OmniGen2 이미지 편집",
|
||||
"image_omnigen2_t2i": "OmniGen2 텍스트 투 이미지",
|
||||
"sd3_5_large_blur": "SD3.5 대형 블러",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 대형 캐니 컨트롤넷",
|
||||
"sd3_5_large_depth": "SD3.5 대형 깊이",
|
||||
"sd3_5_simple_example": "SD3.5 간단 예제",
|
||||
"sdxl_refiner_prompt_example": "SDXL 리파이너 프롬프트",
|
||||
"sdxl_revision_text_prompts": "SDXL Revision 텍스트 프롬프트",
|
||||
"sdxl_revision_zero_positive": "SDXL Revision Zero Positive",
|
||||
"sdxl_simple_example": "간단한 SDXL 예제",
|
||||
"sdxl_simple_example": "SDXL 간단 예제",
|
||||
"sdxlturbo_example": "SDXL 터보"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 인페인트",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 텍스트 투 이미지",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 텍스트 투 이미지",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra 텍스트 투 이미지",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 텍스트 투 이미지",
|
||||
"api_luma_photon_i2i": "Luma Photon 이미지 투 이미지",
|
||||
"api_luma_photon_style_ref": "Luma Photon 스타일 참조",
|
||||
"api_openai_image_1_i2i": "OpenAI Image-1 이미지 투 이미지",
|
||||
"api_openai_image_1_inpaint": "OpenAI Image-1 인페인트",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI Image-1 멀티 입력",
|
||||
"api_openai_image_1_t2i": "OpenAI Image-1 텍스트 투 이미지",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft 색상 제어 이미지 생성",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft 스타일 제어 이미지 생성",
|
||||
"api_recraft_vector_gen": "Recraft 벡터 생성",
|
||||
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra 텍스트 투 이미지"
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext 맥스",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext 다중 이미지 입력",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext 프로",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]: 텍스트 투 이미지",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3: 텍스트 투 이미지",
|
||||
"api_luma_photon_i2i": "Luma Photon: 이미지 투 이미지",
|
||||
"api_luma_photon_style_ref": "Luma Photon: 스타일 참조",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI: Dall-E 2 인페인트",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI: Dall-E 2 텍스트 투 이미지",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI: Dall-E 3 텍스트 투 이미지",
|
||||
"api_openai_image_1_i2i": "OpenAI: GPT-Image-1 이미지 투 이미지",
|
||||
"api_openai_image_1_inpaint": "OpenAI: GPT-Image-1 인페인트",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI: GPT-Image-1 멀티 입력",
|
||||
"api_openai_image_1_t2i": "OpenAI: GPT-Image-1 텍스트 투 이미지",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft: 색상 제어 이미지 생성",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft: 스타일 제어 이미지 생성",
|
||||
"api_recraft_vector_gen": "Recraft: 벡터 생성",
|
||||
"api_runway_reference_to_image": "Runway: 참조 투 이미지",
|
||||
"api_runway_text_to_image": "Runway: 텍스트 투 이미지",
|
||||
"api_stability_ai_i2i": "Stability AI: 이미지 투 이미지",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI: SD3.5 이미지 투 이미지",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI: SD3.5 텍스트 투 이미지",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI: Stable Image Ultra 텍스트 투 이미지"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google Gemini: 채팅",
|
||||
"api_openai_chat": "OpenAI: 채팅"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "ESRGAN",
|
||||
"hiresfix_esrgan_workflow": "HiresFix ESRGAN 워크플로우",
|
||||
"hiresfix_latent_workflow": "HiresFix Latent 워크플로우",
|
||||
"latent_upscale_different_prompt_model": "Latent Upscale 다른 프롬프트 모델"
|
||||
"hiresfix_latent_workflow": "업스케일",
|
||||
"latent_upscale_different_prompt_model": "Latent 업스케일 다른 프롬프트 모델"
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "텍스트 -> 비디오 (Hunyuan Video)",
|
||||
"image_to_video": "이미지 -> 동영상",
|
||||
"image_to_video_wan": "Wan 2.1 이미지에서 비디오로",
|
||||
"ltxv_image_to_video": "이미지 -> 동영상 (LTXV)",
|
||||
"ltxv_text_to_video": "텍스트 -> 동영상 (LTXV)",
|
||||
"mochi_text_to_video_example": "텍스트 -> 동영상 (Mochi)",
|
||||
"text_to_video_wan": "Wan 2.1 텍스트를 비디오로",
|
||||
"txt_to_image_to_video": "텍스트 -> 이미지 -> 동영상",
|
||||
"hunyuan_video_text_to_video": "Hunyuan 비디오 텍스트 투 비디오",
|
||||
"image_to_video": "SVD 이미지 투 비디오",
|
||||
"image_to_video_wan": "Wan 2.1 이미지 투 비디오",
|
||||
"ltxv_image_to_video": "LTXV 이미지 투 비디오",
|
||||
"ltxv_text_to_video": "LTXV 텍스트 투 비디오",
|
||||
"mochi_text_to_video_example": "Mochi 텍스트 투 비디오",
|
||||
"text_to_video_wan": "Wan 2.1 텍스트 투 비디오",
|
||||
"txt_to_image_to_video": "SVD 텍스트 투 이미지 투 비디오",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE 참조 투 비디오",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE 텍스트 투 비디오",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE 컨트롤 비디오",
|
||||
"video_wan_vace_flf2v": "Wan VACE 첫-마지막 프레임",
|
||||
"video_wan_vace_inpainting": "Wan VACE 인페인팅",
|
||||
"video_wan_vace_outpainting": "Wan VACE 아웃페인팅",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"wan2_1_fun_control": "Wan 2.1 컨트롤넷",
|
||||
"wan2_1_fun_inp": "Wan 2.1 인페인트"
|
||||
"wan2_1_fun_inp": "Wan 2.1 인페인팅"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "MiniMax 이미지 투 비디오",
|
||||
"api_kling_i2v": "Kling 이미지 투 비디오",
|
||||
"api_luma_i2v": "Luma 이미지 투 비디오",
|
||||
"api_hailuo_minimax_i2v": "MiniMax: 이미지 투 비디오",
|
||||
"api_hailuo_minimax_t2v": "MiniMax: 텍스트 투 비디오",
|
||||
"api_kling_effects": "Kling: 비디오 효과",
|
||||
"api_kling_flf": "Kling: FLF2V",
|
||||
"api_kling_i2v": "Kling: 이미지 투 비디오",
|
||||
"api_luma_i2v": "Luma: 이미지 투 비디오",
|
||||
"api_luma_t2v": "Luma: 텍스트 투 비디오",
|
||||
"api_moonvalley_image_to_video": "Moonvalley: 이미지 투 비디오",
|
||||
"api_moonvalley_text_to_video": "Moonvalley: 텍스트 투 비디오",
|
||||
"api_pika_i2v": "Pika: 이미지 투 비디오",
|
||||
"api_pika_scene": "Pika 장면: 이미지 투 비디오",
|
||||
"api_pixverse_t2v": "PixVerse 텍스트 투 비디오",
|
||||
"api_pixverse_template_i2v": "PixVerse Template Effects: 이미지 투 비디오",
|
||||
"api_veo2_i2v": "Veo2 이미지 투 비디오"
|
||||
"api_pixverse_i2v": "PixVerse: 이미지 투 비디오",
|
||||
"api_pixverse_t2v": "PixVerse: 텍스트 투 비디오",
|
||||
"api_pixverse_template_i2v": "PixVerse 템플릿: 이미지 투 비디오",
|
||||
"api_runway_first_last_frame": "Runway: 첫-마지막 프레임 투 비디오",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway: Gen3a Turbo 이미지 투 비디오",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway: Gen4 Turbo 이미지 투 비디오",
|
||||
"api_veo2_i2v": "Veo2: 이미지 투 비디오"
|
||||
}
|
||||
},
|
||||
"templateDescription": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D 2mv로 여러 뷰에서 모델을 생성합니다.",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D 2mv turbo로 여러 뷰에서 모델을 생성합니다.",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D 2.0으로 단일 뷰에서 모델을 생성합니다.",
|
||||
"stable_zero123_example": "단일 이미지에서 3D 뷰를 생성합니다."
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0으로 단일 이미지에서 3D 모델을 생성합니다.",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D 2.0 MV로 여러 뷰에서 3D 모델을 생성합니다.",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D 2.0 MV Turbo로 여러 뷰에서 3D 모델을 생성합니다.",
|
||||
"stable_zero123_example": "Stable Zero123으로 단일 이미지에서 3D 뷰를 생성합니다."
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin AI로 단일 사진에서 정밀한 3D 모델을 생성합니다.",
|
||||
"api_rodin_multiview_to_model": "Rodin의 다각도 재구성으로 종합적인 3D 모델을 만듭니다.",
|
||||
"api_tripo_image_to_model": "Tripo 엔진으로 2D 이미지에서 전문가용 3D 에셋을 생성합니다.",
|
||||
"api_tripo_multiview_to_model": "Tripo의 고급 스캐너로 여러 각도에서 3D 모델을 만듭니다.",
|
||||
"api_tripo_text_to_model": "Tripo의 텍스트 기반 모델링으로 설명에서 3D 오브젝트를 만듭니다."
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "영역을 통해 이미지 구성을 제어합니다.",
|
||||
"area_composition_reversed": "영역 구성 워크플로우를 반대로 적용합니다.",
|
||||
"area_composition_square_area_for_subject": "일관된 피사체 배치를 만듭니다."
|
||||
"area_composition": "정의된 영역으로 구성을 제어하여 이미지를 생성합니다.",
|
||||
"area_composition_square_area_for_subject": "일관된 피사체 배치를 위해 영역 구성을 활용해 이미지를 생성합니다."
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "텍스트 설명으로 오디오를 생성합니다."
|
||||
"audio_ace_step_1_m2m_editing": "ACE-Step v1 M2M으로 기존 곡의 스타일과 가사를 변경합니다.",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1로 텍스트 프롬프트에서 연주곡을 생성합니다.",
|
||||
"audio_ace_step_1_t2a_song": "ACE-Step v1로 텍스트 프롬프트에서 노래(보컬 포함)를 생성하며, 다국어 및 스타일 커스터마이징을 지원합니다.",
|
||||
"audio_stable_audio_example": "Stable Audio로 텍스트 프롬프트에서 오디오를 생성합니다."
|
||||
},
|
||||
"Basics": {
|
||||
"default": "텍스트 설명으로 이미지를 생성합니다.",
|
||||
"embedding_example": "일관된 스타일을 위해 텍스트 인버전을 사용합니다.",
|
||||
"gligen_textbox_example": "객체의 위치와 크기를 지정합니다.",
|
||||
"default": "텍스트 프롬프트로 이미지를 생성합니다.",
|
||||
"embedding_example": "일관된 스타일을 위해 텍스트 인버전을 사용하여 이미지를 생성합니다.",
|
||||
"gligen_textbox_example": "텍스트 박스를 사용해 객체의 위치를 정밀하게 지정하여 이미지를 생성합니다.",
|
||||
"image2image": "텍스트 프롬프트를 사용하여 기존 이미지를 변환합니다.",
|
||||
"inpain_model_outpainting": "이미지의 원래 경계를 넘어 확장합니다.",
|
||||
"inpaint_example": "이미지의 특정 부분을 자연스럽게 편집합니다.",
|
||||
"lora": "특정 스타일이나 주제를 위해 LoRA 모델을 적용합니다.",
|
||||
"lora_multiple": "여러 LoRA 모델을 결합하여 독특한 결과를 만듭니다."
|
||||
"inpaint_model_outpainting": "이미지를 원래 경계 너머로 확장합니다.",
|
||||
"lora": "특정 스타일이나 주제를 위한 LoRA 모델로 이미지를 생성합니다.",
|
||||
"lora_multiple": "여러 LoRA 모델을 결합하여 이미지를 생성합니다."
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "포즈 참조로 이미지를 생성합니다.",
|
||||
"controlnet_example": "참조 이미지를 사용해 이미지 생성을 제어합니다.",
|
||||
"depth_controlnet": "깊이 인식 이미지 생성을 합니다.",
|
||||
"depth_t2i_adapter": "T2I 어댑터로 깊이 인식 이미지를 빠르게 생성합니다.",
|
||||
"mixing_controlnets": "여러 ControlNet 모델을 결합합니다."
|
||||
"2_pass_pose_worship": "ControlNet으로 포즈 참조를 활용해 이미지를 생성합니다.",
|
||||
"controlnet_example": "ControlNet으로 스크리블 참조 이미지를 활용해 이미지를 생성합니다.",
|
||||
"depth_controlnet": "ControlNet으로 깊이 정보를 활용해 이미지를 생성합니다.",
|
||||
"depth_t2i_adapter": "T2I 어댑터로 깊이 정보를 활용해 이미지를 생성합니다.",
|
||||
"mixing_controlnets": "여러 ControlNet 모델을 결합해 이미지를 생성합니다."
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "검출된 경계선으로 이미지를 생성합니다.",
|
||||
"flux_depth_lora_example": "깊이 인식 LoRA 를 이용해 이미지를 생성합니다.",
|
||||
"flux_dev_checkpoint_example": "FLUX Dev 모델로 이미지를 생성합니다.",
|
||||
"flux_fill_inpaint_example": "이미지의 누락된 부분을 채웁니다.",
|
||||
"flux_fill_outpaint_example": "FLUX 아웃페인팅으로 이미지를 확장합니다.",
|
||||
"flux_redux_model_example": "참조 이미지의 스타일을 가이드 이미지 생성에 적용합니다.",
|
||||
"flux_schnell": "FLUX Schnell 모델로 이미지를 빠르게 생성합니다."
|
||||
"flux_canny_model_example": "Flux Canny로 에지 감지에 따라 이미지를 생성합니다.",
|
||||
"flux_depth_lora_example": "Flux LoRA로 깊이 정보를 활용해 이미지를 생성합니다.",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8 양자화 버전으로 이미지를 생성합니다. VRAM이 제한된 장치에 적합하며, 모델 파일 하나만 필요하지만 화질은 전체 버전보다 약간 낮습니다.",
|
||||
"flux_dev_full_text_to_image": "Flux Dev 전체 버전으로 고품질 이미지를 생성합니다. 더 많은 VRAM과 여러 모델 파일이 필요하지만, 최고의 프롬프트 반영력과 화질을 제공합니다.",
|
||||
"flux_fill_inpaint_example": "Flux 인페인팅으로 이미지의 누락된 부분을 채웁니다.",
|
||||
"flux_fill_outpaint_example": "Flux 아웃페인팅으로 이미지를 경계 너머로 확장합니다.",
|
||||
"flux_kontext_dev_basic": "Flux Kontext의 전체 노드 표시로 이미지를 편집합니다. 워크플로우 학습에 적합합니다.",
|
||||
"flux_kontext_dev_grouped": "노드가 그룹화된 Flux Kontext의 간소화 버전으로 작업 공간이 더 깔끔합니다.",
|
||||
"flux_redux_model_example": "Flux Redux로 참조 이미지의 스타일을 전송하여 이미지를 생성합니다.",
|
||||
"flux_schnell": "Flux Schnell fp8 양자화 버전으로 이미지를 빠르게 생성합니다. 저사양 하드웨어에 이상적이며, 4단계만으로 이미지를 생성할 수 있습니다.",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell 전체 버전으로 이미지를 빠르게 생성합니다. Apache2.0 라이선스를 사용하며, 4단계만으로 좋은 화질을 유지합니다."
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 모델로 이미지를 편집합니다.",
|
||||
"hidream_i1_dev": "HiDream I1 Dev 모델로 이미지를 생성합니다.",
|
||||
"hidream_i1_fast": "HiDream I1 Fast 모델로 이미지를 빠르게 생성합니다.",
|
||||
"hidream_i1_full": "HiDream I1 Full 모델로 이미지를 생성합니다.",
|
||||
"sd3_5_large_blur": "SD 3.5 모델로 흐릿한 참조 이미지에서 이미지를 생성합니다.",
|
||||
"sd3_5_large_canny_controlnet_example": "Canny 에지 이미지를 통해 SD 3.5 모델 이미지 생성을 가이드합니다.",
|
||||
"sd3_5_large_depth": "깊이 인식 이미지를 통해 SD 3.5 모델 이미지 생성을 가이드합니다.",
|
||||
"sd3_5_simple_example": "SD 3.5 모델로 이미지를 생성합니다.",
|
||||
"sdxl_refiner_prompt_example": "SDXL 결과물을 리파이너로 향상시킵니다.",
|
||||
"sdxl_revision_text_prompts": "참조 이미지의 개념을 SDXL 이미지 생성에 적용합니다.",
|
||||
"sdxl_revision_zero_positive": "참조 이미지와 함께 텍스트 프롬프트를 추가하여 SDXL 이미지 생성을 가이드합니다.",
|
||||
"sdxl_simple_example": "SDXL 모델로 고품질 이미지를 생성합니다.",
|
||||
"sdxlturbo_example": "SDXL Turbo 모델로 1 스텝으로 이미지를 생성합니다."
|
||||
"hidream_e1_full": "HiDream E1 - 전문적인 자연어 이미지 편집 모델로 이미지를 편집합니다.",
|
||||
"hidream_i1_dev": "HiDream I1 Dev - 28 스텝의 균형 잡힌 버전으로, 중간급 하드웨어에 적합합니다.",
|
||||
"hidream_i1_fast": "HiDream I1 Fast - 16 스텝의 경량 버전으로, 저사양 하드웨어에서 빠른 미리보기에 적합합니다.",
|
||||
"hidream_i1_full": "HiDream I1 Full - 50 스텝의 완전 버전으로, 최고의 품질을 제공합니다.",
|
||||
"image_chroma_text_to_image": "Chroma는 flux에서 수정된 모델로, 아키텍처에 일부 변화가 있습니다.",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos-Predict2 2B T2I로 물리적으로 정확하고 고해상도, 디테일이 풍부한 이미지를 생성합니다.",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth로 고효율 단안 깊이 추정 및 디테일 보존이 뛰어난 zero-shot 이미지를 생성합니다.",
|
||||
"image_omnigen2_image_edit": "OmniGen2의 고급 이미지 편집 기능과 텍스트 렌더링 지원으로 자연어 지시로 이미지를 편집합니다.",
|
||||
"image_omnigen2_t2i": "OmniGen2의 통합 7B 멀티모달 모델과 듀얼 패스 아키텍처로 텍스트 프롬프트에서 고품질 이미지를 생성합니다.",
|
||||
"sd3_5_large_blur": "SD 3.5로 흐릿한 참조 이미지를 활용해 이미지를 생성합니다.",
|
||||
"sd3_5_large_canny_controlnet_example": "SD 3.5 Canny ControlNet으로 에지 감지에 따라 이미지를 생성합니다.",
|
||||
"sd3_5_large_depth": "SD 3.5로 깊이 정보를 활용해 이미지를 생성합니다.",
|
||||
"sd3_5_simple_example": "SD 3.5로 이미지를 생성합니다.",
|
||||
"sdxl_refiner_prompt_example": "SDXL 리파이너 모델로 이미지를 향상시킵니다.",
|
||||
"sdxl_revision_text_prompts": "SDXL Revision으로 참조 이미지의 개념을 전송하여 이미지를 생성합니다.",
|
||||
"sdxl_revision_zero_positive": "SDXL Revision으로 텍스트 프롬프트와 참조 이미지를 함께 사용해 이미지를 생성합니다.",
|
||||
"sdxl_simple_example": "SDXL로 고품질 이미지를 생성합니다.",
|
||||
"sdxlturbo_example": "SDXL Turbo로 한 번에 이미지를 생성합니다."
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 API로 이미지를 인페인팅합니다.",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 API로 텍스트 설명에서 이미지를 생성합니다.",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 API로 텍스트 설명에서 이미지를 생성합니다.",
|
||||
"api_bfl_flux_pro_t2i": "FLUX.1 [pro]의 뛰어난 프롬프트 반영, 시각적 품질, 이미지 디테일, 다양성으로 이미지를 생성합니다.",
|
||||
"api_ideogram_v3_t2i": "고품질 이미지-프롬프트 일치, 포토리얼리즘, 텍스트 렌더링으로 이미지를 생성합니다. 전문가 수준의 로고, 홍보 포스터, 랜딩 페이지 컨셉, 제품 사진 등을 만드세요. 정교한 배경, 섬세한 조명과 색상, 사실적인 환경 디테일로 세련된 공간 구성을 손쉽게 제작할 수 있습니다.",
|
||||
"api_bfl_flux_1_kontext_max_image": "Flux.1 Kontext 맥스 이미지로 이미지를 편집합니다.",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "여러 이미지를 입력하고 Flux.1 Kontext로 편집합니다.",
|
||||
"api_bfl_flux_1_kontext_pro_image": "Flux.1 Kontext 프로 이미지로 이미지를 편집합니다.",
|
||||
"api_bfl_flux_pro_t2i": "FLUX.1 Pro로 뛰어난 프롬프트 반영과 시각적 품질로 이미지를 생성합니다.",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3로 뛰어난 프롬프트 일치, 포토리얼리즘, 텍스트 렌더링으로 전문가 수준의 이미지를 생성합니다.",
|
||||
"api_luma_photon_i2i": "이미지와 프롬프트를 조합하여 이미지 생성을 가이드합니다.",
|
||||
"api_luma_photon_style_ref": "정확한 제어로 스타일 참조를 적용하고 혼합합니다. Luma Photon은 각 참조 이미지의 본질을 포착하여, 전문적인 품질을 유지하면서 독특한 시각적 요소를 결합할 수 있습니다.",
|
||||
"api_openai_image_1_i2i": "GPT Image 1 API로 이미지에서 이미지를 생성합니다.",
|
||||
"api_openai_image_1_inpaint": "GPT Image 1 API로 이미지를 인페인팅합니다.",
|
||||
"api_openai_image_1_multi_inputs": "GPT Image 1 API로 여러 입력을 사용해 이미지를 생성합니다.",
|
||||
"api_openai_image_1_t2i": "GPT Image 1 API로 텍스트 설명에서 이미지를 생성합니다.",
|
||||
"api_recraft_image_gen_with_color_control": "여러 이미지에 재사용할 맞춤 팔레트를 만들거나 각 사진마다 색상을 직접 선택하세요. 브랜드의 색상 팔레트에 맞추고, 독창적인 비주얼을 제작하세요.",
|
||||
"api_recraft_image_gen_with_style_control": "시각적 예시로 스타일을 제어하고, 위치를 맞추며, 객체를 미세 조정하세요. 스타일을 저장 및 공유하여 브랜드 일관성을 유지할 수 있습니다.",
|
||||
"api_recraft_vector_gen": "텍스트 프롬프트에서 Recraft의 AI 벡터 생성기로 벡터 이미지를 만드세요. 로고, 포스터, 아이콘 세트, 광고, 배너, 목업 등 최고의 품질의 벡터 아트를 제작할 수 있습니다. 선명하고 고품질의 SVG 파일로 디자인을 완성하세요. 앱이나 웹사이트를 위한 브랜드 벡터 일러스트를 몇 초 만에 만드세요.",
|
||||
"api_stability_sd3_t2i": "1메가픽셀 해상도에서 전문가용 고품질 이미지를 생성합니다. 프롬프트 반영이 우수합니다."
|
||||
"api_luma_photon_style_ref": "Luma Photon으로 스타일 참조를 혼합하여 정밀하게 제어하며 이미지를 생성합니다.",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI Dall-E 2 API로 인페인팅을 통해 이미지를 편집합니다.",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI Dall-E 2 API로 텍스트 프롬프트에서 이미지를 생성합니다.",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI Dall-E 3 API로 텍스트 프롬프트에서 이미지를 생성합니다.",
|
||||
"api_openai_image_1_i2i": "OpenAI GPT Image 1 API로 입력 이미지에서 이미지를 생성합니다.",
|
||||
"api_openai_image_1_inpaint": "OpenAI GPT Image 1 API로 인페인팅을 통해 이미지를 편집합니다.",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI GPT Image 1 API로 여러 입력에서 이미지를 생성합니다.",
|
||||
"api_openai_image_1_t2i": "OpenAI GPT Image 1 API로 텍스트 프롬프트에서 이미지를 생성합니다.",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft로 맞춤 색상 팔레트와 브랜드 비주얼로 이미지를 생성합니다.",
|
||||
"api_recraft_image_gen_with_style_control": "시각적 예시로 스타일을 제어하고, 위치를 맞추며, 객체를 미세 조정합니다. 스타일을 저장 및 공유하여 브랜드 일관성을 유지할 수 있습니다.",
|
||||
"api_recraft_vector_gen": "Recraft의 AI 벡터 생성기로 텍스트 프롬프트에서 고품질 벡터 이미지를 생성합니다.",
|
||||
"api_runway_reference_to_image": "Runway의 AI로 참조 스타일과 구성을 기반으로 새 이미지를 생성합니다.",
|
||||
"api_runway_text_to_image": "Runway의 AI 모델로 텍스트 프롬프트에서 고품질 이미지를 생성합니다.",
|
||||
"api_stability_ai_i2i": "Stability AI로 고품질 이미지 변환 및 스타일 전환을 지원합니다.",
|
||||
"api_stability_ai_sd3_5_i2i": "1메가픽셀 해상도에서 전문가용 고품질 이미지를 생성합니다. 프롬프트 반영이 우수합니다.",
|
||||
"api_stability_ai_sd3_5_t2i": "1메가픽셀 해상도에서 전문가용 고품질 이미지를 생성합니다. 프롬프트 반영이 우수합니다.",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "1메가픽셀 해상도에서 전문가용 고품질 이미지를 생성합니다. 프롬프트 반영이 우수합니다."
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google Gemini의 멀티모달 AI와 추론 능력을 경험하세요.",
|
||||
"api_openai_chat": "OpenAI의 고급 언어 모델과 대화하세요."
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "업스케일 모델로 이미지 품질을 향상합니다.",
|
||||
"hiresfix_esrgan_workflow": "중간 단계에서 업스케일 모델을 사용합니다.",
|
||||
"hiresfix_latent_workflow": "latent 공간에서 이미지 품질을 향상합니다.",
|
||||
"latent_upscale_different_prompt_model": "업스케일과 프롬프트 변경을 여러 번에 걸쳐 적용합니다."
|
||||
"esrgan_example": "ESRGAN 모델로 이미지 품질을 향상합니다.",
|
||||
"hiresfix_esrgan_workflow": "중간 생성 단계에서 ESRGAN 모델로 업스케일합니다.",
|
||||
"hiresfix_latent_workflow": "Latent 공간에서 이미지 품질을 향상합니다.",
|
||||
"latent_upscale_different_prompt_model": "여러 번의 생성 패스에서 프롬프트를 변경하며 업스케일합니다."
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Hunyuan 모델을 사용하여 비디오를 생성합니다.",
|
||||
"image_to_video": "이미지를 애니메이션 비디오로 변환합니다.",
|
||||
"image_to_video_wan": "이미지로부터 빠르게 비디오를 생성합니다.",
|
||||
"ltxv_image_to_video": "정지 이미지를 비디오로 변환합니다.",
|
||||
"ltxv_text_to_video": "텍스트 설명으로 비디오를 생성합니다.",
|
||||
"mochi_text_to_video_example": "Mochi 모델로 비디오를 생성합니다.",
|
||||
"text_to_video_wan": "텍스트 설명으로 빠르게 비디오를 생성합니다.",
|
||||
"txt_to_image_to_video": "텍스트로 이미지를 생성한 후 비디오로 변환합니다.",
|
||||
"wan2_1_flf2v_720_f16": "첫 프레임과 마지막 프레임을 제어하여 비디오를 생성합니다.",
|
||||
"wan2_1_fun_control": "포즈, 깊이, 에지 등으로 비디오 생성을 가이드합니다.",
|
||||
"wan2_1_fun_inp": "시작 및 종료 프레임으로 비디오를 생성합니다."
|
||||
"hunyuan_video_text_to_video": "Hunyuan 모델로 텍스트 프롬프트에서 비디오를 생성합니다.",
|
||||
"image_to_video": "정지 이미지로부터 비디오를 생성합니다.",
|
||||
"image_to_video_wan": "Wan 2.1로 이미지에서 비디오를 생성합니다.",
|
||||
"ltxv_image_to_video": "정지 이미지로부터 비디오를 생성합니다.",
|
||||
"ltxv_text_to_video": "텍스트 프롬프트로 비디오를 생성합니다.",
|
||||
"mochi_text_to_video_example": "Mochi 모델로 텍스트 프롬프트에서 비디오를 생성합니다.",
|
||||
"text_to_video_wan": "Wan 2.1로 텍스트 프롬프트에서 비디오를 생성합니다.",
|
||||
"txt_to_image_to_video": "텍스트 프롬프트로 이미지를 생성한 후 비디오로 변환합니다.",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos-Predict2 2B Video2World로 물리적으로 정확하고 고해상도, 일관성 있는 비디오 시뮬레이션을 생성합니다.",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "14B 전체 모델로 고급 카메라 제어가 가능한 고품질 비디오를 생성합니다.",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B 모델로 시네마틱 카메라 움직임이 있는 동적 비디오를 생성합니다.",
|
||||
"video_wan_vace_14B_ref2v": "참조 이미지의 스타일과 내용을 일치시키는 비디오를 생성합니다.",
|
||||
"video_wan_vace_14B_t2v": "VACE-14B 모델로 480p 및 720p 고품질 비디오를 생성합니다.",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE로 입력 비디오와 참조 이미지를 제어하여 비디오를 생성합니다.",
|
||||
"video_wan_vace_flf2v": "시작 및 종료 프레임을 정의하여 부드러운 비디오 전환을 생성합니다. 사용자 지정 키프레임 시퀀스를 지원합니다.",
|
||||
"video_wan_vace_inpainting": "특정 영역을 편집하면서 주변 내용을 보존하는 비디오를 생성합니다. 객체 제거 또는 교체에 적합합니다.",
|
||||
"video_wan_vace_outpainting": "Wan VACE 아웃페인팅으로 비디오 크기를 확장하여 비디오를 생성합니다.",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V로 첫 프레임과 마지막 프레임을 제어하여 비디오를 생성합니다.",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet으로 포즈, 깊이, 에지 제어로 비디오를 생성합니다.",
|
||||
"wan2_1_fun_inp": "Wan 2.1 인페인팅으로 시작 및 종료 프레임에서 비디오를 생성합니다."
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "이미지와 텍스트로 정교한 비디오를 생성합니다. CGI 통합, 바이럴 AI 허깅 등 트렌디한 사진 효과도 포함됩니다. 다양한 비디오 스타일과 테마로 창의적인 비전을 실현하세요.",
|
||||
"api_kling_i2v": "동작, 표정, 카메라 움직임에 대한 프롬프트 반영이 뛰어난 비디오를 생성합니다. 이제 복잡한 프롬프트와 연속 동작도 지원되어, 장면의 연출자가 될 수 있습니다.",
|
||||
"api_luma_i2v": "정지 이미지를 즉시 고품질 애니메이션으로 만드세요.",
|
||||
"api_pika_scene": "여러 이미지를 재료로 사용하여 모두를 포함하는 비디오를 생성합니다.",
|
||||
"api_hailuo_minimax_i2v": "MiniMax로 이미지와 텍스트에서 CGI 통합된 정교한 비디오를 생성합니다.",
|
||||
"api_hailuo_minimax_t2v": "MiniMax의 고급 AI로 텍스트 프롬프트에서 고품질 비디오를 직접 생성합니다. 다양한 스타일과 전문 CGI 효과로 창의적인 비주얼을 만듭니다.",
|
||||
"api_kling_effects": "Kling으로 이미지에 시각 효과를 적용해 동적 비디오를 생성합니다.",
|
||||
"api_kling_flf": "첫 프레임과 마지막 프레임을 제어하여 비디오를 생성합니다.",
|
||||
"api_kling_i2v": "Kling으로 동작, 표정, 카메라 움직임에 대한 프롬프트 반영이 뛰어난 비디오를 생성합니다.",
|
||||
"api_luma_i2v": "정지 이미지를 즉시 고품질 애니메이션으로 만듭니다.",
|
||||
"api_luma_t2v": "간단한 프롬프트로 고품질 비디오를 생성합니다.",
|
||||
"api_moonvalley_image_to_video": "라이선스 데이터로만 학습된 모델로 이미지를 사용해 시네마틱 1080p 비디오를 생성합니다.",
|
||||
"api_moonvalley_text_to_video": "라이선스 데이터로만 학습된 모델로 텍스트 프롬프트에서 시네마틱 1080p 비디오를 생성합니다.",
|
||||
"api_pika_i2v": "Pika AI로 단일 정지 이미지에서 부드러운 애니메이션 비디오를 생성합니다.",
|
||||
"api_pika_scene": "Pika Scenes로 여러 입력 이미지를 포함하는 비디오를 생성합니다.",
|
||||
"api_pixverse_i2v": "PixVerse로 정지 이미지에서 모션과 효과가 있는 동적 비디오를 생성합니다.",
|
||||
"api_pixverse_t2v": "정확한 프롬프트 해석과 놀라운 비디오 다이내믹스로 비디오를 생성합니다.",
|
||||
"api_pixverse_template_i2v": "정지 이미지를 동적 비디오로 변환하고 모션과 효과를 추가합니다.",
|
||||
"api_pixverse_template_i2v": "PixVerse로 정지 이미지에서 모션과 효과가 있는 동적 비디오를 생성합니다.",
|
||||
"api_runway_first_last_frame": "Runway로 두 키프레임 사이의 부드러운 비디오 전환을 생성합니다.",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway Gen3a Turbo로 정지 이미지에서 시네마틱 비디오를 생성합니다.",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway Gen4 Turbo로 이미지에서 동적 비디오를 생성합니다.",
|
||||
"api_veo2_i2v": "Google Veo2 API로 이미지에서 비디오를 생성합니다."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -326,7 +326,8 @@
|
||||
"Bottom": "하단",
|
||||
"Disabled": "비활성화",
|
||||
"Top": "상단"
|
||||
}
|
||||
},
|
||||
"tooltip": "메뉴 바 위치입니다. 모바일 기기에서는 메뉴가 항상 상단에 표시됩니다."
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "노드 정의 유효성 검사 (느림)",
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
"noTasksFoundMessage": "В очереди нет задач.",
|
||||
"noWorkflowsFound": "Рабочие процессы не найдены.",
|
||||
"nodes": "Узлы",
|
||||
"nodesRunning": "запущено узлов",
|
||||
"ok": "ОК",
|
||||
"openNewIssue": "Открыть новую проблему",
|
||||
"overwrite": "Перезаписать",
|
||||
@@ -1200,55 +1201,75 @@
|
||||
"Flux": "Flux",
|
||||
"Image": "Изображение",
|
||||
"Image API": "Image API",
|
||||
"LLM API": "LLM API",
|
||||
"Upscaling": "Увеличение разрешения",
|
||||
"Video": "Видео",
|
||||
"Video API": "Video API"
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Hunyuan3D Многовидовой",
|
||||
"hunyuan-3d-turbo": "Hunyuan3D Турбо",
|
||||
"hunyuan3d-non-multiview-train": "Hunyuan3D",
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D Многовидовой",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D Турбо",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin: изображение в модель",
|
||||
"api_rodin_multiview_to_model": "Rodin: многовидовой в модель",
|
||||
"api_tripo_image_to_model": "Tripo: изображение в модель",
|
||||
"api_tripo_multiview_to_model": "Tripo: многовидовой в модель",
|
||||
"api_tripo_text_to_model": "Tripo: текст в модель"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Композиция области",
|
||||
"area_composition_reversed": "Обратная композиция области",
|
||||
"area_composition_square_area_for_subject": "Композиция области квадратной области для субъекта"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Stable Audio"
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 M2M редактирование",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1 текст в инструментальную музыку",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1 текст в песню",
|
||||
"audio_stable_audio_example": "Stable Audio"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "Генерация изображений",
|
||||
"embedding_example": "Встраивание",
|
||||
"gligen_textbox_example": "Gligen Textbox",
|
||||
"image2image": "Изображение в изображение",
|
||||
"inpain_model_outpainting": "Inpaint Model Outpainting",
|
||||
"inpaint_example": "Inpaint",
|
||||
"lora": "Lora",
|
||||
"lora_multiple": "Lora Multiple"
|
||||
"inpaint_model_outpainting": "Outpainting",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "LoRA Multiple"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "2 Pass Pose Worship",
|
||||
"controlnet_example": "ControlNet",
|
||||
"controlnet_example": "Scribble ControlNet",
|
||||
"depth_controlnet": "Depth ControlNet",
|
||||
"depth_t2i_adapter": "Depth T2I Adapter",
|
||||
"mixing_controlnets": "Mixing ControlNets"
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Flux Canny Model",
|
||||
"flux_depth_lora_example": "Flux Depth Lora",
|
||||
"flux_dev_checkpoint_example": "Flux Dev",
|
||||
"flux_depth_lora_example": "Flux Depth LoRA",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8",
|
||||
"flux_dev_full_text_to_image": "Flux Dev полный текст в изображение",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev (Базовый)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev (Групповой)",
|
||||
"flux_redux_model_example": "Flux Redux Model",
|
||||
"flux_schnell": "Flux Schnell"
|
||||
"flux_schnell": "Flux Schnell fp8",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell полный текст в изображение"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 Full",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full",
|
||||
"image_chroma_text_to_image": "Chroma текст в изображение",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B T2I",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth",
|
||||
"image_omnigen2_image_edit": "OmniGen2 редактирование изображения",
|
||||
"image_omnigen2_t2i": "OmniGen2 текст в изображение",
|
||||
"sd3_5_large_blur": "SD3.5 Большое размытие",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Большой Canny ControlNet",
|
||||
"sd3_5_large_depth": "SD3.5 Большая глубина",
|
||||
@@ -1260,21 +1281,33 @@
|
||||
"sdxlturbo_example": "SDXL Turbo"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2: дорисовка",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2: текст в изображение",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3: текст в изображение",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra текст в изображение",
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext Max",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext несколько изображений",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext Pro",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]: текст в изображение",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3: текст в изображение",
|
||||
"api_luma_photon_i2i": "Luma Photon: изображение в изображение",
|
||||
"api_luma_photon_style_ref": "Luma Photon: стиль по образцу",
|
||||
"api_openai_image_1_i2i": "OpenAI Image-1: изображение в изображение",
|
||||
"api_openai_image_1_inpaint": "OpenAI Image-1: дорисовка",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI Image-1: несколько входов",
|
||||
"api_openai_image_1_t2i": "OpenAI Image-1: текст в изображение",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI: Dall-E 2 дорисовка",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI: Dall-E 2 текст в изображение",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI: Dall-E 3 текст в изображение",
|
||||
"api_openai_image_1_i2i": "OpenAI: GPT-Image-1 изображение в изображение",
|
||||
"api_openai_image_1_inpaint": "OpenAI: GPT-Image-1 дорисовка",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI: GPT-Image-1 несколько входов",
|
||||
"api_openai_image_1_t2i": "OpenAI: GPT-Image-1 текст в изображение",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft: генерация изображения с управлением цветом",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft: генерация изображения с управлением стилем",
|
||||
"api_recraft_vector_gen": "Recraft: генерация векторного изображения",
|
||||
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra текст в изображение"
|
||||
"api_runway_reference_to_image": "Runway: референс в изображение",
|
||||
"api_runway_text_to_image": "Runway: текст в изображение",
|
||||
"api_stability_ai_i2i": "Stability AI: изображение в изображение",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI: SD3.5 изображение в изображение",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI: SD3.5 текст в изображение",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI: Stable Image Ultra текст в изображение"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google Gemini: чат",
|
||||
"api_openai_chat": "OpenAI: чат"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "ESRGAN",
|
||||
@@ -1283,127 +1316,198 @@
|
||||
"latent_upscale_different_prompt_model": "Latent Upscale Different Prompt Model"
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video",
|
||||
"image_to_video": "Изображение в видео",
|
||||
"image_to_video_wan": "Wan 2.1 Изображение в Видео",
|
||||
"ltxv_image_to_video": "LTXV Image to Video",
|
||||
"ltxv_text_to_video": "LTXV Text to Video",
|
||||
"mochi_text_to_video_example": "Mochi Text to Video",
|
||||
"text_to_video_wan": "Wan 2.1 Текст в Видео",
|
||||
"txt_to_image_to_video": "Текст в изображение в видео",
|
||||
"hunyuan_video_text_to_video": "Hunyuan видео текст в видео",
|
||||
"image_to_video": "SVD изображение в видео",
|
||||
"image_to_video_wan": "Wan 2.1 изображение в видео",
|
||||
"ltxv_image_to_video": "LTXV изображение в видео",
|
||||
"ltxv_text_to_video": "LTXV текст в видео",
|
||||
"mochi_text_to_video_example": "Mochi текст в видео",
|
||||
"text_to_video_wan": "Wan 2.1 текст в видео",
|
||||
"txt_to_image_to_video": "SVD текст в изображение в видео",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE референс в видео",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE текст в видео",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE контроль видео",
|
||||
"video_wan_vace_flf2v": "Wan VACE первый-последний кадр",
|
||||
"video_wan_vace_inpainting": "Wan VACE Inpainting",
|
||||
"video_wan_vace_outpainting": "Wan VACE Outpainting",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet",
|
||||
"wan2_1_fun_inp": "Wan 2.1 Inpainting"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "MiniMax: изображение в видео",
|
||||
"api_hailuo_minimax_t2v": "MiniMax: текст в видео",
|
||||
"api_kling_effects": "Kling: видеоэффекты",
|
||||
"api_kling_flf": "Kling: FLF2V",
|
||||
"api_kling_i2v": "Kling: изображение в видео",
|
||||
"api_luma_i2v": "Luma: изображение в видео",
|
||||
"api_luma_t2v": "Luma: текст в видео",
|
||||
"api_moonvalley_image_to_video": "Moonvalley: изображение в видео",
|
||||
"api_moonvalley_text_to_video": "Moonvalley: текст в видео",
|
||||
"api_pika_i2v": "Pika: изображение в видео",
|
||||
"api_pika_scene": "Pika Scenes: изображения в видео",
|
||||
"api_pixverse_i2v": "PixVerse: изображение в видео",
|
||||
"api_pixverse_t2v": "PixVerse: текст в видео",
|
||||
"api_pixverse_template_i2v": "PixVerse Template Effects: изображение в видео",
|
||||
"api_pixverse_template_i2v": "PixVerse Templates: изображение в видео",
|
||||
"api_runway_first_last_frame": "Runway: первый последний кадр в видео",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway: Gen3a Turbo изображение в видео",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway: Gen4 Turbo изображение в видео",
|
||||
"api_veo2_i2v": "Veo2: изображение в видео"
|
||||
}
|
||||
},
|
||||
"templateDescription": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "Используйте Hunyuan3D 2mv для генерации моделей по нескольким видам.",
|
||||
"hunyuan-3d-turbo": "Используйте Hunyuan3D 2mv turbo для генерации моделей по нескольким видам.",
|
||||
"hunyuan3d-non-multiview-train": "Используйте Hunyuan3D 2.0 для генерации моделей по одному виду.",
|
||||
"stable_zero123_example": "Генерируйте 3D-виды по одному изображению."
|
||||
"3d_hunyuan3d_image_to_model": "Генерируйте 3D-модели по одному изображению с помощью Hunyuan3D 2.0.",
|
||||
"3d_hunyuan3d_multiview_to_model": "Генерируйте 3D-модели по нескольким видам с помощью Hunyuan3D 2.0 MV.",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Генерируйте 3D-модели по нескольким видам с помощью Hunyuan3D 2.0 MV Turbo.",
|
||||
"stable_zero123_example": "Генерируйте 3D-виды по одному изображению с помощью Stable Zero123."
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Генерируйте детализированные 3D-модели по одной фотографии с помощью Rodin AI.",
|
||||
"api_rodin_multiview_to_model": "Создавайте полные 3D-модели по нескольким видам с помощью Rodin.",
|
||||
"api_tripo_image_to_model": "Генерируйте профессиональные 3D-ассеты по 2D-изображениям с помощью Tripo.",
|
||||
"api_tripo_multiview_to_model": "Создавайте 3D-модели по нескольким ракурсам с помощью продвинутого сканера Tripo.",
|
||||
"api_tripo_text_to_model": "Создавайте 3D-объекты по текстовым описаниям с помощью Tripo."
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Управляйте композицией изображения с помощью областей.",
|
||||
"area_composition_reversed": "Обратный рабочий процесс композиции областей.",
|
||||
"area_composition_square_area_for_subject": "Создавайте стабильное размещение объекта."
|
||||
"area_composition": "Генерируйте изображения, управляя композицией с помощью определённых областей.",
|
||||
"area_composition_square_area_for_subject": "Генерируйте изображения с постоянным размещением объекта с помощью композиции областей."
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Генерируйте аудио по текстовым описаниям."
|
||||
"audio_ace_step_1_m2m_editing": "Редактируйте существующие песни, меняя стиль и текст с помощью ACE-Step v1 M2M.",
|
||||
"audio_ace_step_1_t2a_instrumentals": "Генерируйте инструментальную музыку по тексту с помощью ACE-Step v1.",
|
||||
"audio_ace_step_1_t2a_song": "Генерируйте песни с вокалом по тексту с помощью ACE-Step v1, поддержка разных языков и стилей.",
|
||||
"audio_stable_audio_example": "Генерируйте аудио по текстовым описаниям с помощью Stable Audio."
|
||||
},
|
||||
"Basics": {
|
||||
"default": "Генерируйте изображения по текстовым описаниям.",
|
||||
"embedding_example": "Используйте текстовую инверсию для единых стилей.",
|
||||
"gligen_textbox_example": "Указывайте расположение и размер объектов.",
|
||||
"embedding_example": "Генерируйте изображения с помощью текстовой инверсии для единых стилей.",
|
||||
"gligen_textbox_example": "Генерируйте изображения с точным размещением объектов с помощью текстовых блоков.",
|
||||
"image2image": "Преобразуйте существующие изображения с помощью текстовых подсказок.",
|
||||
"inpain_model_outpainting": "Расширяйте изображения за пределы их исходных границ.",
|
||||
"inpaint_example": "Редактируйте отдельные части изображений без швов.",
|
||||
"lora": "Применяйте LoRA-модели для специализированных стилей или объектов.",
|
||||
"lora_multiple": "Комбинируйте несколько LoRA-моделей для уникальных результатов."
|
||||
"inpaint_model_outpainting": "Расширяйте изображения за пределы их исходных границ.",
|
||||
"lora": "Генерируйте изображения с помощью моделей LoRA для специализированных стилей или объектов.",
|
||||
"lora_multiple": "Генерируйте изображения, комбинируя несколько моделей LoRA."
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "Генерируйте изображения по референсам поз.",
|
||||
"controlnet_example": "Управляйте генерацией изображений с помощью референсных изображений.",
|
||||
"depth_controlnet": "Создавайте изображения с учетом глубины.",
|
||||
"depth_t2i_adapter": "Быстро генерируйте изображения с глубиной с помощью T2I-адаптера.",
|
||||
"mixing_controlnets": "Комбинируйте несколько моделей ControlNet вместе."
|
||||
"2_pass_pose_worship": "Генерируйте изображения по референсам поз с помощью ControlNet.",
|
||||
"controlnet_example": "Генерируйте изображения, используя референсы-каракулы с помощью ControlNet.",
|
||||
"depth_controlnet": "Генерируйте изображения, используя информацию о глубине с помощью ControlNet.",
|
||||
"depth_t2i_adapter": "Генерируйте изображения, используя информацию о глубине с помощью адаптера T2I.",
|
||||
"mixing_controlnets": "Генерируйте изображения, комбинируя несколько моделей ControlNet."
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Генерируйте изображения по детекции границ.",
|
||||
"flux_depth_lora_example": "Создавайте изображения с глубиной с помощью LoRA.",
|
||||
"flux_dev_checkpoint_example": "Создавайте изображения с помощью Flux development models.",
|
||||
"flux_fill_inpaint_example": "Заполняйте отсутствующие части изображений.",
|
||||
"flux_fill_outpaint_example": "Расширяйте изображения с помощью Flux outpainting.",
|
||||
"flux_redux_model_example": "Передавайте стиль с референсного изображения для управления генерацией с помощью Flux.",
|
||||
"flux_schnell": "Быстро генерируйте изображения с Flux Schnell."
|
||||
"flux_canny_model_example": "Генерируйте изображения, используя детекцию границ с помощью Flux Canny.",
|
||||
"flux_depth_lora_example": "Генерируйте изображения, используя информацию о глубине с помощью Flux LoRA.",
|
||||
"flux_dev_checkpoint_example": "Генерируйте изображения с помощью Flux Dev fp8 (квантованная версия). Подходит для устройств с ограниченной VRAM, требуется только один файл модели, но качество немного ниже, чем у полной версии.",
|
||||
"flux_dev_full_text_to_image": "Генерируйте высококачественные изображения с помощью полной версии Flux Dev. Требуется больше VRAM и несколько файлов моделей, но обеспечивается лучшее следование подсказкам и качество.",
|
||||
"flux_fill_inpaint_example": "Заполняйте отсутствующие части изображений с помощью Flux inpainting.",
|
||||
"flux_fill_outpaint_example": "Расширяйте изображения за пределы исходных границ с помощью Flux outpainting.",
|
||||
"flux_kontext_dev_basic": "Редактируйте изображения с помощью Flux Kontext (все узлы видимы), идеально для изучения рабочего процесса.",
|
||||
"flux_kontext_dev_grouped": "Упрощённая версия Flux Kontext с группированными узлами для более чистого рабочего пространства.",
|
||||
"flux_redux_model_example": "Генерируйте изображения, перенося стиль с референсных изображений с помощью Flux Redux.",
|
||||
"flux_schnell": "Быстро генерируйте изображения с помощью Flux Schnell fp8 (квантованная версия). Идеально для слабого железа, требуется всего 4 шага.",
|
||||
"flux_schnell_full_text_to_image": "Быстро генерируйте изображения с помощью полной версии Flux Schnell. Лицензия Apache2.0, всего 4 шага при хорошем качестве."
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "Редактируйте изображения с HiDream E1.",
|
||||
"hidream_i1_dev": "Генерируйте изображения с HiDream I1 Dev.",
|
||||
"hidream_i1_fast": "Быстро генерируйте изображения с HiDream I1.",
|
||||
"hidream_i1_full": "Генерируйте изображения с HiDream I1.",
|
||||
"sd3_5_large_blur": "Генерируйте изображения по размытым референсам с SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Используйте детекцию границ для управления генерацией с SD 3.5.",
|
||||
"sd3_5_large_depth": "Создавайте изображения с глубиной с SD 3.5.",
|
||||
"sd3_5_simple_example": "Генерируйте изображения с SD 3.5.",
|
||||
"sdxl_refiner_prompt_example": "Улучшайте результаты SDXL с помощью refiners.",
|
||||
"sdxl_revision_text_prompts": "Передавайте концепции с референсных изображений для управления генерацией с SDXL.",
|
||||
"sdxl_revision_zero_positive": "Добавляйте текстовые подсказки вместе с референсными изображениями для управления генерацией с SDXL.",
|
||||
"sdxl_simple_example": "Создавайте высококачественные изображения с SDXL.",
|
||||
"sdxlturbo_example": "Генерируйте изображения за один шаг с SDXL Turbo."
|
||||
"hidream_e1_full": "HiDream E1 — профессиональная модель для редактирования изображений на естественном языке.",
|
||||
"hidream_i1_dev": "HiDream I1 Dev — сбалансированная версия (28 шагов), подходит для среднего железа.",
|
||||
"hidream_i1_fast": "HiDream I1 Fast — облегчённая версия (16 шагов), быстрая генерация на слабых ПК.",
|
||||
"hidream_i1_full": "HiDream I1 Full — полная версия (50 шагов), максимальное качество.",
|
||||
"image_chroma_text_to_image": "Chroma — модифицированная версия Flux с изменённой архитектурой.",
|
||||
"image_cosmos_predict2_2B_t2i": "Генерируйте изображения с помощью Cosmos-Predict2 2B T2I — физически точная, высокодетализированная генерация.",
|
||||
"image_lotus_depth_v1_1": "Используйте Lotus Depth в ComfyUI для эффективной и детализированной оценки глубины по одному изображению.",
|
||||
"image_omnigen2_image_edit": "Редактируйте изображения с помощью естественно-языковых инструкций и расширенных возможностей OmniGen2.",
|
||||
"image_omnigen2_t2i": "Генерируйте высококачественные изображения по тексту с помощью унифицированной 7B мультимодальной модели OmniGen2 с двухпутевой архитектурой.",
|
||||
"sd3_5_large_blur": "Генерируйте изображения, используя размытые референсы с помощью SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Генерируйте изображения, используя детекцию границ с помощью SD 3.5 Canny ControlNet.",
|
||||
"sd3_5_large_depth": "Генерируйте изображения, используя информацию о глубине с помощью SD 3.5.",
|
||||
"sd3_5_simple_example": "Генерируйте изображения с помощью SD 3.5.",
|
||||
"sdxl_refiner_prompt_example": "Улучшайте изображения SDXL с помощью моделей-улучшателей (refiner).",
|
||||
"sdxl_revision_text_prompts": "Передавайте концепции с референсных изображений для генерации с помощью SDXL Revision.",
|
||||
"sdxl_revision_zero_positive": "Генерируйте изображения, используя текстовые подсказки и референсы с помощью SDXL Revision.",
|
||||
"sdxl_simple_example": "Генерируйте высококачественные изображения с помощью SDXL.",
|
||||
"sdxlturbo_example": "Генерируйте изображения за один шаг с помощью SDXL Turbo."
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Используйте Dall-E 2 API для инпейнта изображений.",
|
||||
"api-openai-dall-e-2-t2i": "Используйте Dall-E 2 API для генерации изображений по текстовым описаниям.",
|
||||
"api-openai-dall-e-3-t2i": "Используйте Dall-E 3 API для генерации изображений по текстовым описаниям.",
|
||||
"api_bfl_flux_pro_t2i": "Создавайте изображения с помощью FLUX.1 [pro] с отличным следованием подсказкам, высоким качеством, детализацией и разнообразием.",
|
||||
"api_ideogram_v3_t2i": "Генерируйте изображения с высоким соответствием подсказкам, фотореализмом и рендерингом текста. Создавайте профессиональные логотипы, промо-постеры, концепты лендингов, продуктовые фото и многое другое. Легко создавайте сложные пространственные композиции с детализированным фоном, точным освещением и реалистичной средой.",
|
||||
"api_bfl_flux_1_kontext_max_image": "Редактируйте изображения с помощью Flux.1 Kontext max image.",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "Вводите несколько изображений и редактируйте их с помощью Flux.1 Kontext.",
|
||||
"api_bfl_flux_1_kontext_pro_image": "Редактируйте изображения с помощью Flux.1 Kontext pro image.",
|
||||
"api_bfl_flux_pro_t2i": "Генерируйте изображения с отличным следованием подсказкам и качеством с помощью FLUX.1 Pro.",
|
||||
"api_ideogram_v3_t2i": "Генерируйте профессиональные изображения с отличным соответствием подсказкам, фотореализмом и рендерингом текста с помощью Ideogram V3.",
|
||||
"api_luma_photon_i2i": "Управляйте генерацией изображений с помощью комбинации изображений и подсказки.",
|
||||
"api_luma_photon_style_ref": "Применяйте и смешивайте стили с точным контролем. Luma Photon захватывает суть каждого референса, позволяя комбинировать уникальные визуальные элементы с профессиональным качеством.",
|
||||
"api_openai_image_1_i2i": "Используйте GPT Image 1 API для генерации изображений по изображениям.",
|
||||
"api_openai_image_1_inpaint": "Используйте GPT Image 1 API для инпейнта изображений.",
|
||||
"api_openai_image_1_multi_inputs": "Используйте GPT Image 1 API с несколькими входами для генерации изображений.",
|
||||
"api_openai_image_1_t2i": "Используйте GPT Image 1 API для генерации изображений по текстовым описаниям.",
|
||||
"api_recraft_image_gen_with_color_control": "Создайте собственную палитру для повторного использования или подберите цвета для каждого фото. Совместите фирменную палитру и создайте уникальные визуалы.",
|
||||
"api_recraft_image_gen_with_style_control": "Контролируйте стиль с помощью визуальных примеров, выравнивайте объекты и настраивайте детали. Сохраняйте и делитесь стилями для идеального брендирования.",
|
||||
"api_recraft_vector_gen": "Преобразуйте текстовую подсказку в векторное изображение с помощью AI-генератора Recraft. Создавайте лучшие векторные арты для логотипов, постеров, иконок, баннеров и мокапов. Дорабатывайте дизайн с помощью качественных SVG-файлов. Создавайте фирменные векторные иллюстрации для приложений и сайтов за секунды.",
|
||||
"api_stability_sd3_t2i": "Генерируйте высококачественные изображения с отличным следованием подсказкам. Идеально для профессионального использования при разрешении 1 мегапиксель."
|
||||
"api_luma_photon_style_ref": "Генерируйте изображения, смешивая стили с точным контролем с помощью Luma Photon.",
|
||||
"api_openai_dall_e_2_inpaint": "Редактируйте изображения с помощью инпейнта в OpenAI Dall-E 2 API.",
|
||||
"api_openai_dall_e_2_t2i": "Генерируйте изображения по тексту с помощью OpenAI Dall-E 2 API.",
|
||||
"api_openai_dall_e_3_t2i": "Генерируйте изображения по тексту с помощью OpenAI Dall-E 3 API.",
|
||||
"api_openai_image_1_i2i": "Генерируйте изображения по изображениям с помощью OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_inpaint": "Редактируйте изображения с помощью инпейнта в OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_multi_inputs": "Генерируйте изображения по нескольким входам с помощью OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_t2i": "Генерируйте изображения по тексту с помощью OpenAI GPT Image 1 API.",
|
||||
"api_recraft_image_gen_with_color_control": "Генерируйте изображения с пользовательскими палитрами и фирменными визуалами с помощью Recraft.",
|
||||
"api_recraft_image_gen_with_style_control": "Контролируйте стиль с помощью визуальных примеров, выравнивайте объекты и настраивайте детали. Сохраняйте и делитесь стилями для брендовой целостности.",
|
||||
"api_recraft_vector_gen": "Генерируйте векторные изображения высокого качества по тексту с помощью AI-генератора Recraft.",
|
||||
"api_runway_reference_to_image": "Генерируйте новые изображения на основе стиля и композиции референса с помощью Runway.",
|
||||
"api_runway_text_to_image": "Генерируйте изображения высокого качества по тексту с помощью модели Runway AI.",
|
||||
"api_stability_ai_i2i": "Преобразуйте изображения с высоким качеством с помощью Stability AI, идеально для профессионального редактирования и передачи стиля.",
|
||||
"api_stability_ai_sd3_5_i2i": "Генерируйте изображения высокого качества с отличным следованием подсказкам. Идеально для профессионального использования при разрешении 1 мегапиксель.",
|
||||
"api_stability_ai_sd3_5_t2i": "Генерируйте изображения высокого качества с отличным следованием подсказкам. Идеально для профессионального использования при разрешении 1 мегапиксель.",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Генерируйте изображения высокого качества с отличным следованием подсказкам. Идеально для профессионального использования при разрешении 1 мегапиксель."
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Испытайте мультимодальный ИИ Google с возможностями рассуждения Gemini.",
|
||||
"api_openai_chat": "Общайтесь с продвинутыми языковыми моделями OpenAI для интеллектуальных диалогов."
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "Используйте модели апскейлинга для повышения качества изображений.",
|
||||
"hiresfix_esrgan_workflow": "Используйте модели апскейлинга на промежуточных этапах.",
|
||||
"hiresfix_latent_workflow": "Улучшайте качество изображений в latent space.",
|
||||
"latent_upscale_different_prompt_model": "Увеличивайте и меняйте подсказку на разных проходах."
|
||||
"esrgan_example": "Увеличивайте изображения с помощью моделей ESRGAN для повышения качества.",
|
||||
"hiresfix_esrgan_workflow": "Увеличивайте изображения с помощью моделей ESRGAN на промежуточных этапах.",
|
||||
"hiresfix_latent_workflow": "Увеличивайте изображения, улучшая качество в латентном пространстве.",
|
||||
"latent_upscale_different_prompt_model": "Увеличивайте изображения, меняя подсказки между проходами."
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "Генерируйте видео с помощью модели Hunyuan.",
|
||||
"image_to_video": "Преобразуйте изображения в анимированные видео.",
|
||||
"image_to_video_wan": "Быстро генерируйте видео из изображений.",
|
||||
"ltxv_image_to_video": "Преобразуйте статичные изображения в видео.",
|
||||
"ltxv_text_to_video": "Генерируйте видео по текстовым описаниям.",
|
||||
"mochi_text_to_video_example": "Создавайте видео с помощью модели Mochi.",
|
||||
"text_to_video_wan": "Быстро генерируйте видео по текстовым описаниям.",
|
||||
"txt_to_image_to_video": "Генерируйте изображения по тексту, а затем преобразуйте их в видео.",
|
||||
"wan2_1_flf2v_720_f16": "Генерируйте видео, контролируя первый и последний кадры.",
|
||||
"wan2_1_fun_control": "Управляйте генерацией видео с помощью позы, глубины, границ и других параметров.",
|
||||
"wan2_1_fun_inp": "Создавайте видео по начальному и конечному кадрам."
|
||||
"hunyuan_video_text_to_video": "Генерируйте видео по тексту с помощью модели Hunyuan.",
|
||||
"image_to_video": "Генерируйте видео по статичным изображениям.",
|
||||
"image_to_video_wan": "Генерируйте видео по изображениям с помощью Wan 2.1.",
|
||||
"ltxv_image_to_video": "Генерируйте видео по статичным изображениям.",
|
||||
"ltxv_text_to_video": "Генерируйте видео по тексту.",
|
||||
"mochi_text_to_video_example": "Генерируйте видео по тексту с помощью модели Mochi.",
|
||||
"text_to_video_wan": "Генерируйте видео по тексту с помощью Wan 2.1.",
|
||||
"txt_to_image_to_video": "Сначала создайте изображения по тексту, затем преобразуйте их в видео.",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Генерируйте видео с помощью Cosmos-Predict2 2B Video2World — физически точные, высокодетализированные и последовательные симуляции.",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Генерируйте видео высокого качества с расширенным управлением камерой с помощью полной модели 14B.",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Генерируйте динамичные видео с кинематографическим движением камеры с помощью Wan 2.1 Fun Camera 1.3B.",
|
||||
"video_wan_vace_14B_ref2v": "Создавайте видео, соответствующие стилю и содержанию референсного изображения.",
|
||||
"video_wan_vace_14B_t2v": "Преобразуйте текстовые описания в видео высокого качества. Поддержка 480p и 720p с моделью VACE-14B.",
|
||||
"video_wan_vace_14B_v2v": "Генерируйте видео, управляя входными видео и референсами с помощью Wan VACE.",
|
||||
"video_wan_vace_flf2v": "Создавайте плавные переходы, задавая начальный и конечный кадры. Поддержка пользовательских последовательностей ключевых кадров.",
|
||||
"video_wan_vace_inpainting": "Редактируйте отдельные области видео, сохраняя окружающее содержимое.",
|
||||
"video_wan_vace_outpainting": "Генерируйте расширенные видео, увеличивая размер с помощью Wan VACE outpainting.",
|
||||
"wan2_1_flf2v_720_f16": "Генерируйте видео, контролируя первый и последний кадры с помощью Wan 2.1 FLF2V.",
|
||||
"wan2_1_fun_control": "Генерируйте видео, управляя позой, глубиной и границами с помощью Wan 2.1 ControlNet.",
|
||||
"wan2_1_fun_inp": "Генерируйте видео по начальному и конечному кадрам с помощью Wan 2.1 inpainting."
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "Создавайте изысканные видео из изображений и текста, включая CGI и трендовые эффекты, такие как viral AI hugging. Выбирайте стили и темы для вашего креатива.",
|
||||
"api_kling_i2v": "Создавайте видео с отличным следованием подсказкам для действий, эмоций и движений камеры. Теперь поддерживаются сложные подсказки с последовательными действиями — вы режиссёр своей сцены.",
|
||||
"api_hailuo_minimax_i2v": "Генерируйте изысканные видео по изображениям и тексту с CGI-интеграцией с помощью MiniMax.",
|
||||
"api_hailuo_minimax_t2v": "Генерируйте высококачественные видео напрямую по тексту. Используйте возможности MiniMax для создания разнообразных визуальных историй с профессиональными CGI-эффектами.",
|
||||
"api_kling_effects": "Генерируйте динамичные видео, применяя визуальные эффекты к изображениям с помощью Kling.",
|
||||
"api_kling_flf": "Генерируйте видео, контролируя первый и последний кадры.",
|
||||
"api_kling_i2v": "Генерируйте видео с отличным следованием подсказкам для действий, эмоций и движений камеры с помощью Kling.",
|
||||
"api_luma_i2v": "Преобразуйте статичные изображения в волшебные анимации высокого качества.",
|
||||
"api_pika_scene": "Используйте несколько изображений как ингредиенты и генерируйте видео, включающие их все.",
|
||||
"api_luma_t2v": "Генерируйте высококачественные видео по простым подсказкам.",
|
||||
"api_moonvalley_image_to_video": "Генерируйте кинематографические видео 1080p по изображению с помощью модели, обученной только на лицензированных данных.",
|
||||
"api_moonvalley_text_to_video": "Генерируйте кинематографические видео 1080p по тексту с помощью модели, обученной только на лицензированных данных.",
|
||||
"api_pika_i2v": "Генерируйте плавные анимированные видео по одному изображению с помощью Pika AI.",
|
||||
"api_pika_scene": "Генерируйте видео, включающие несколько входных изображений, с помощью Pika Scenes.",
|
||||
"api_pixverse_i2v": "Генерируйте динамичные видео по статичным изображениям с движением и эффектами с помощью PixVerse.",
|
||||
"api_pixverse_t2v": "Генерируйте видео с точной интерпретацией подсказок и впечатляющей динамикой.",
|
||||
"api_pixverse_template_i2v": "Преобразует статичные изображения в динамичные видео с движением и эффектами.",
|
||||
"api_veo2_i2v": "Используйте Google Veo2 API для генерации видео из изображений."
|
||||
"api_pixverse_template_i2v": "Генерируйте динамичные видео по статичным изображениям с движением и эффектами с помощью PixVerse.",
|
||||
"api_runway_first_last_frame": "Генерируйте плавные переходы между двумя ключевыми кадрами с помощью Runway.",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Генерируйте кинематографические видео по статичным изображениям с помощью Runway Gen3a Turbo.",
|
||||
"api_runway_gen4_turo_image_to_video": "Генерируйте динамичные видео по изображениям с помощью Runway Gen4 Turbo.",
|
||||
"api_veo2_i2v": "Генерируйте видео по изображениям с помощью Google Veo2 API."
|
||||
}
|
||||
},
|
||||
"title": "Начните с шаблона"
|
||||
|
||||
@@ -326,7 +326,8 @@
|
||||
"Bottom": "Внизу",
|
||||
"Disabled": "Отключено",
|
||||
"Top": "Вверху"
|
||||
}
|
||||
},
|
||||
"tooltip": "Расположение панели меню. На мобильных устройствах меню всегда отображается вверху."
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "Проверка определений нод (медленно)",
|
||||
|
||||
249
src/locales/zh-TW/commands.json
Normal file
@@ -0,0 +1,249 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "檢查更新"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "開啟自訂節點資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "開啟輸入資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "開啟日誌資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "開啟 extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "開啟模型資料夾"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "開啟輸出資料夾"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "開啟開發者工具"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "桌面版使用指南"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "退出"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "重新安裝"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "重新啟動"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "瀏覽範本"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "新增編輯模型步驟"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "刪除選取項目"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "將視圖適應至所選節點"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "將選取的節點下移"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "左移選取的節點"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "右移選取的節點"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "上移選取的節點"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "重設視圖"
|
||||
},
|
||||
"Comfy_Canvas_Resize": {
|
||||
"label": "調整所選節點大小"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLinkVisibility": {
|
||||
"label": "畫布切換連結可見性"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "畫布切換鎖定"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "略過/取消略過選取的節點"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
|
||||
"label": "收合/展開選取的節點"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
|
||||
"label": "停用/啟用選取的節點"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
|
||||
"label": "釘選/取消釘選已選取的節點"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "釘選/取消釘選已選項目"
|
||||
},
|
||||
"Comfy_Canvas_ZoomIn": {
|
||||
"label": "放大"
|
||||
},
|
||||
"Comfy_Canvas_ZoomOut": {
|
||||
"label": "縮小"
|
||||
},
|
||||
"Comfy_ClearPendingTasks": {
|
||||
"label": "清除待處理任務"
|
||||
},
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "清除工作流程"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "聯絡支援"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "複製目前工作流程"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "匯出工作流程"
|
||||
},
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "匯出工作流程(API 格式)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "提供回饋"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "將選取內容轉換為子圖"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "調整群組以符合內容"
|
||||
},
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "群組所選節點"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "將選取的節點轉換為群組節點"
|
||||
},
|
||||
"Comfy_GroupNode_ManageGroupNodes": {
|
||||
"label": "管理群組節點"
|
||||
},
|
||||
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
|
||||
"label": "取消群組所選群組節點"
|
||||
},
|
||||
"Comfy_Help_AboutComfyUI": {
|
||||
"label": "開啟關於 ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyOrgDiscord": {
|
||||
"label": "開啟 Comfy-Org Discord"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "開啟 ComfyUI 文件"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "開啟 ComfyUI 論壇"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "開啟 ComfyUI 問題追蹤"
|
||||
},
|
||||
"Comfy_Interrupt": {
|
||||
"label": "中斷"
|
||||
},
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "載入預設工作流程"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "切換自訂節點管理器"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切換自訂節點管理器進度條"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "為選取的節點開啟 Mask 編輯器"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新增空白工作流程"
|
||||
},
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "開啟工作流程"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "將提示詞加入佇列"
|
||||
},
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "將提示詞加入佇列前方"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "佇列所選的輸出節點"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "重做"
|
||||
},
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "重新整理節點定義"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "儲存工作流程"
|
||||
},
|
||||
"Comfy_SaveWorkflowAs": {
|
||||
"label": "另存工作流程"
|
||||
},
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "顯示設定對話框"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "切換主題(深色/淺色)"
|
||||
},
|
||||
"Comfy_Undo": {
|
||||
"label": "復原"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "開啟登入對話框"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "登出"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "關閉當前工作流程"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "下一個已開啟的工作流程"
|
||||
},
|
||||
"Workspace_PreviousOpenedWorkflow": {
|
||||
"label": "上次開啟的工作流程"
|
||||
},
|
||||
"Workspace_SearchBox_Toggle": {
|
||||
"label": "切換搜尋框"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel": {
|
||||
"label": "切換下方面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_command-terminal": {
|
||||
"label": "切換終端機底部面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_logs-terminal": {
|
||||
"label": "切換日誌底部面板"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "切換專注模式"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "切換模型庫側邊欄",
|
||||
"tooltip": "模型庫"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_node-library": {
|
||||
"label": "切換節點庫側邊欄",
|
||||
"tooltip": "節點庫"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "切換佇列側邊欄",
|
||||
"tooltip": "佇列"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "切換工作流程側邊欄",
|
||||
"tooltip": "工作流程"
|
||||
}
|
||||
}
|
||||
1599
src/locales/zh-TW/main.json
Normal file
8660
src/locales/zh-TW/nodeDefs.json
Normal file
413
src/locales/zh-TW/settings.json
Normal file
@@ -0,0 +1,413 @@
|
||||
{
|
||||
"Comfy-Desktop_AutoUpdate": {
|
||||
"name": "自動檢查更新"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "傳送匿名使用統計資料"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "PyPI 安裝鏡像站",
|
||||
"tooltip": "預設 pip 安裝鏡像站"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python 安裝鏡像站",
|
||||
"tooltip": "受管理的 Python 安裝檔會從 Astral 的 python-build-standalone 專案下載。這個變數可以設定為鏡像站的 URL,以便從不同來源下載 Python 安裝檔。所提供的 URL 會取代 https://github.com/astral-sh/python-build-standalone/releases/download,例如:https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz。若要從本機目錄讀取發行版本,請使用 file:// URL 格式。"
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Torch 安裝鏡像站",
|
||||
"tooltip": "PyTorch 的 pip 安裝鏡像站"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "視窗樣式",
|
||||
"options": {
|
||||
"custom": "自訂",
|
||||
"default": "預設"
|
||||
},
|
||||
"tooltip": "自訂:以 ComfyUI 的頂部選單取代系統標題列"
|
||||
},
|
||||
"Comfy_Canvas_BackgroundImage": {
|
||||
"name": "畫布背景圖片",
|
||||
"tooltip": "畫布背景的圖片網址。你可以在輸出面板中右鍵點擊圖片並選擇「設為背景」來使用,或是使用上傳按鈕上傳你自己的圖片。"
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "顯示選取工具箱"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "清除工作流程時需要確認"
|
||||
},
|
||||
"Comfy_DOMClippingEnabled": {
|
||||
"name": "啟用 DOM 元素裁剪(啟用後可能會降低效能)"
|
||||
},
|
||||
"Comfy_DevMode": {
|
||||
"name": "啟用開發者模式選項(API 儲存等)"
|
||||
},
|
||||
"Comfy_DisableFloatRounding": {
|
||||
"name": "停用預設浮點數元件四捨五入。",
|
||||
"tooltip": "(需重新載入頁面)當後端節點已設定四捨五入時,無法停用四捨五入。"
|
||||
},
|
||||
"Comfy_DisableSliders": {
|
||||
"name": "停用節點元件滑桿"
|
||||
},
|
||||
"Comfy_EditAttention_Delta": {
|
||||
"name": "Ctrl+上/下 精確調整"
|
||||
},
|
||||
"Comfy_EnableTooltips": {
|
||||
"name": "啟用工具提示"
|
||||
},
|
||||
"Comfy_EnableWorkflowViewRestore": {
|
||||
"name": "在工作流程中儲存並還原畫布位置與縮放等級"
|
||||
},
|
||||
"Comfy_FloatRoundingPrecision": {
|
||||
"name": "浮點元件小數點位數 [0 = 自動]。",
|
||||
"tooltip": "(需重新載入頁面)"
|
||||
},
|
||||
"Comfy_Graph_CanvasInfo": {
|
||||
"name": "在左下角顯示畫布資訊(fps 等)"
|
||||
},
|
||||
"Comfy_Graph_CanvasMenu": {
|
||||
"name": "顯示圖形畫布選單"
|
||||
},
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "啟用快速縮放快捷鍵(Ctrl + Shift + 拖曳)"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "連結中點標記",
|
||||
"options": {
|
||||
"Arrow": "箭頭",
|
||||
"Circle": "圓圈",
|
||||
"None": "無"
|
||||
}
|
||||
},
|
||||
"Comfy_Graph_ZoomSpeed": {
|
||||
"name": "畫布縮放速度"
|
||||
},
|
||||
"Comfy_GroupSelectedNodes_Padding": {
|
||||
"name": "群組所選節點間距"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "雙擊群組標題以編輯"
|
||||
},
|
||||
"Comfy_LinkRelease_Action": {
|
||||
"name": "釋放連結時的動作(無修飾鍵)",
|
||||
"options": {
|
||||
"context menu": "右鍵選單",
|
||||
"no action": "無動作",
|
||||
"search box": "搜尋框"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRelease_ActionShift": {
|
||||
"name": "連結釋放時的動作(Shift)",
|
||||
"options": {
|
||||
"context menu": "右鍵選單",
|
||||
"no action": "無動作",
|
||||
"search box": "搜尋框"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRenderMode": {
|
||||
"name": "連結渲染模式",
|
||||
"options": {
|
||||
"Hidden": "隱藏",
|
||||
"Linear": "線性",
|
||||
"Spline": "曲線",
|
||||
"Straight": "直線"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初始背景顏色",
|
||||
"tooltip": "控制 3D 場景的預設背景顏色。此設定決定新建立 3D 元件時的背景外觀,但每個元件在建立後都可單獨調整。"
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "初始相機類型",
|
||||
"options": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透視"
|
||||
},
|
||||
"tooltip": "控制新建 3D 元件時,相機預設為透視或正交。此預設值在建立後仍可針對每個元件單獨切換。"
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "燈光調整增量",
|
||||
"tooltip": "控制在 3D 場景中調整燈光強度時的增量大小。較小的步進值可讓您更細緻地調整燈光,較大的值則每次調整會有更明顯的變化。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "初始光源強度",
|
||||
"tooltip": "設定 3D 場景中燈光的預設亮度等級。此數值決定新建立 3D 元件時燈光照亮物體的強度,但每個元件在建立後都可以個別調整。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "最大光照強度",
|
||||
"tooltip": "設定 3D 場景中允許的最大光照強度值。這會定義在調整任何 3D 小工具照明時可設定的最高亮度上限。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "光源強度下限",
|
||||
"tooltip": "設定 3D 場景中允許的最小光源強度值。這會定義在調整任何 3D 控制元件照明時可設定的最低亮度限制。"
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "初始網格可見性",
|
||||
"tooltip": "控制在建立新的 3D 元件時,網格是否預設可見。此預設值在建立後仍可針對每個元件單獨切換。"
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "初始預覽可見性",
|
||||
"tooltip": "控制當新建 3D 元件時,預覽畫面預設是否顯示。此預設值在元件建立後仍可針對每個元件單獨切換。"
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "語言"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
|
||||
"name": "筆刷調整速度倍數",
|
||||
"tooltip": "控制調整筆刷大小與硬度時的變化速度。數值越高,變化越快。"
|
||||
},
|
||||
"Comfy_MaskEditor_UseDominantAxis": {
|
||||
"name": "鎖定筆刷調整至主軸",
|
||||
"tooltip": "啟用後,筆刷調整只會根據你移動較多的方向,分別影響大小或硬度"
|
||||
},
|
||||
"Comfy_MaskEditor_UseNewEditor": {
|
||||
"name": "使用新遮罩編輯器",
|
||||
"tooltip": "切換到新遮罩編輯器介面"
|
||||
},
|
||||
"Comfy_ModelLibrary_AutoLoadAll": {
|
||||
"name": "自動載入所有模型資料夾",
|
||||
"tooltip": "若為開啟,當你打開模型庫時,所有資料夾將自動載入(這可能會導致載入時延遲)。若為關閉,只有在你點擊根目錄下的模型資料夾時才會載入。"
|
||||
},
|
||||
"Comfy_ModelLibrary_NameFormat": {
|
||||
"name": "在模型庫樹狀檢視中顯示的名稱",
|
||||
"options": {
|
||||
"filename": "filename",
|
||||
"title": "title"
|
||||
},
|
||||
"tooltip": "選擇「filename」可在模型清單中顯示簡化的原始檔名(不含目錄或「.safetensors」副檔名)。選擇「title」則顯示可設定的模型中繼資料標題。"
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "節點 ID 標籤模式",
|
||||
"options": {
|
||||
"None": "無",
|
||||
"Show all": "全部顯示"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "節點生命週期徽章模式",
|
||||
"options": {
|
||||
"None": "無",
|
||||
"Show all": "顯示全部"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "節點來源徽章模式",
|
||||
"options": {
|
||||
"Hide built-in": "隱藏內建",
|
||||
"None": "無",
|
||||
"Show all": "全部顯示"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_ShowApiPricing": {
|
||||
"name": "顯示 API 節點價格標籤"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "節點搜尋框實作",
|
||||
"options": {
|
||||
"default": "預設",
|
||||
"litegraph (legacy)": "litegraph(舊版)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "節點預覽",
|
||||
"tooltip": "僅適用於預設實作"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "在搜尋結果中顯示節點分類",
|
||||
"tooltip": "僅適用於預設實作"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "在搜尋結果中顯示節點 ID 名稱",
|
||||
"tooltip": "僅適用於預設實作"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "在搜尋結果中顯示節點頻率",
|
||||
"tooltip": "僅適用於預設實作"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "節點建議數量",
|
||||
"tooltip": "僅適用於 litegraph 搜尋框/右鍵選單"
|
||||
},
|
||||
"Comfy_Node_AllowImageSizeDraw": {
|
||||
"name": "在圖片預覽下方顯示寬度 × 高度"
|
||||
},
|
||||
"Comfy_Node_AutoSnapLinkToSlot": {
|
||||
"name": "自動吸附連結到節點插槽",
|
||||
"tooltip": "拖曳連結到節點時,連結會自動吸附到節點上可用的輸入插槽"
|
||||
},
|
||||
"Comfy_Node_BypassAllLinksOnDelete": {
|
||||
"name": "刪除節點時保留所有連結",
|
||||
"tooltip": "刪除節點時,嘗試自動重新連接其所有輸入與輸出連結(繞過被刪除的節點)"
|
||||
},
|
||||
"Comfy_Node_DoubleClickTitleToEdit": {
|
||||
"name": "雙擊節點標題以編輯"
|
||||
},
|
||||
"Comfy_Node_MiddleClickRerouteNode": {
|
||||
"name": "中鍵點擊建立新的重導節點"
|
||||
},
|
||||
"Comfy_Node_Opacity": {
|
||||
"name": "節點不透明度"
|
||||
},
|
||||
"Comfy_Node_ShowDeprecated": {
|
||||
"name": "在搜尋中顯示已棄用節點",
|
||||
"tooltip": "已棄用的節點在介面中預設隱藏,但在現有使用這些節點的工作流程中仍可運作。"
|
||||
},
|
||||
"Comfy_Node_ShowExperimental": {
|
||||
"name": "在搜尋中顯示實驗性節點",
|
||||
"tooltip": "實驗性節點會在介面中標註,未來版本可能會有重大變動或被移除。請在正式工作流程中謹慎使用"
|
||||
},
|
||||
"Comfy_Node_SnapHighlightsNode": {
|
||||
"name": "節點高亮顯示對齊",
|
||||
"tooltip": "當拖曳連結到具有可用輸入插槽的節點時,高亮顯示該節點"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "顯示版本更新",
|
||||
"tooltip": "顯示新模型和主要新功能的更新。"
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "指標點擊漂移延遲",
|
||||
"tooltip": "按下指標按鈕後,這是可忽略指標移動的最長時間(以毫秒為單位)。\n\n可防止在點擊時移動指標導致物件被意外推動。"
|
||||
},
|
||||
"Comfy_Pointer_ClickDrift": {
|
||||
"name": "指標點擊漂移(最大距離)",
|
||||
"tooltip": "如果在按住按鈕時指標移動超過此距離,則視為拖曳(而非點擊)。\n\n可防止在點擊時不小心移動指標導致物件被意外推動。"
|
||||
},
|
||||
"Comfy_Pointer_DoubleClickTime": {
|
||||
"name": "雙擊間隔(最大值)",
|
||||
"tooltip": "兩次點擊被視為雙擊的最長間隔時間(毫秒)。增加此數值可協助在雙擊有時未被辨識時改善操作體驗。"
|
||||
},
|
||||
"Comfy_PreviewFormat": {
|
||||
"name": "預覽圖片格式",
|
||||
"tooltip": "在圖片元件中顯示預覽時,將其轉換為輕量級圖片格式,例如 webp、jpeg、webp;50 等。"
|
||||
},
|
||||
"Comfy_PromptFilename": {
|
||||
"name": "儲存工作流程時提示輸入檔案名稱"
|
||||
},
|
||||
"Comfy_QueueButton_BatchCountLimit": {
|
||||
"name": "批次數量上限",
|
||||
"tooltip": "每次按鈕點擊可加入佇列的最大任務數"
|
||||
},
|
||||
"Comfy_Queue_MaxHistoryItems": {
|
||||
"name": "佇列歷史記錄大小",
|
||||
"tooltip": "佇列歷史中顯示的最大任務數量。"
|
||||
},
|
||||
"Comfy_Sidebar_Location": {
|
||||
"name": "側邊欄位置",
|
||||
"options": {
|
||||
"left": "左側",
|
||||
"right": "右側"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_Size": {
|
||||
"name": "側邊欄大小",
|
||||
"options": {
|
||||
"normal": "一般",
|
||||
"small": "小"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "統一側邊欄寬度"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "對齊至格線大小",
|
||||
"tooltip": "當按住 Shift 拖曳或調整節點大小時,節點會對齊到格線,此設定可調整格線的大小。"
|
||||
},
|
||||
"Comfy_TextareaWidget_FontSize": {
|
||||
"name": "文字區塊元件字型大小"
|
||||
},
|
||||
"Comfy_TextareaWidget_Spellcheck": {
|
||||
"name": "文字方塊小工具拼字檢查"
|
||||
},
|
||||
"Comfy_TreeExplorer_ItemPadding": {
|
||||
"name": "樹狀瀏覽器項目間距"
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "使用新選單",
|
||||
"options": {
|
||||
"Bottom": "下方",
|
||||
"Disabled": "停用",
|
||||
"Top": "上方"
|
||||
},
|
||||
"tooltip": "選單列位置。在行動裝置上,選單永遠顯示在頂部。"
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "驗證節點定義(較慢)",
|
||||
"tooltip": "建議節點開發者使用。這會在啟動時驗證所有節點定義。"
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "驗證工作流程"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "元件控制模式",
|
||||
"options": {
|
||||
"after": "佇列後",
|
||||
"before": "佇列前"
|
||||
},
|
||||
"tooltip": "控制元件數值何時更新(隨機、遞增、遞減),可選在提示加入佇列前或後進行。"
|
||||
},
|
||||
"Comfy_Window_UnloadConfirmation": {
|
||||
"name": "關閉視窗時顯示確認提示"
|
||||
},
|
||||
"Comfy_Workflow_AutoSave": {
|
||||
"name": "自動儲存",
|
||||
"options": {
|
||||
"after delay": "延遲後",
|
||||
"off": "關閉"
|
||||
}
|
||||
},
|
||||
"Comfy_Workflow_AutoSaveDelay": {
|
||||
"name": "自動儲存延遲(毫秒)",
|
||||
"tooltip": "僅在自動儲存設為「延遲後」時適用。"
|
||||
},
|
||||
"Comfy_Workflow_ConfirmDelete": {
|
||||
"name": "刪除工作流程時顯示確認視窗"
|
||||
},
|
||||
"Comfy_Workflow_Persist": {
|
||||
"name": "保留工作流程狀態並於頁面重新載入時還原"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingModelsWarning": {
|
||||
"name": "顯示缺少模型警告"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingNodesWarning": {
|
||||
"name": "顯示缺少節點警告"
|
||||
},
|
||||
"Comfy_Workflow_SortNodeIdOnSave": {
|
||||
"name": "儲存工作流程時排序節點 ID"
|
||||
},
|
||||
"Comfy_Workflow_WorkflowTabsPosition": {
|
||||
"name": "已開啟工作流程的位置",
|
||||
"options": {
|
||||
"Sidebar": "側邊欄",
|
||||
"Topbar": "頂部欄",
|
||||
"Topbar (2nd-row)": "頂部欄(第二列)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "低品質渲染縮放臨界值",
|
||||
"tooltip": "當縮小檢視時以低品質渲染圖形"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "最大FPS",
|
||||
"tooltip": "畫布允許渲染的最大每秒幀數。限制GPU使用率,但可能影響流暢度。若設為0,則使用螢幕的更新率。預設值:0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "放大時縮放節點組合小工具選單(清單)"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "新增節點時自動縮小",
|
||||
"tooltip": "建立節點時自動調整為最小尺寸。若停用,新增的節點會略為加寬以顯示元件數值。"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "提示延遲"
|
||||
},
|
||||
"LiteGraph_Pointer_TrackpadGestures": {
|
||||
"name": "啟用觸控板手勢",
|
||||
"tooltip": "此設定可為畫布啟用觸控板模式,允許使用兩指縮放與平移。"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "重導樣條偏移",
|
||||
"tooltip": "貝茲控制點相對於重導中心點的偏移量"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "總是對齊格線"
|
||||
}
|
||||
}
|
||||
@@ -336,6 +336,7 @@
|
||||
"noTasksFoundMessage": "队列中没有任务。",
|
||||
"noWorkflowsFound": "未找到工作流。",
|
||||
"nodes": "节点",
|
||||
"nodesRunning": "节点正在运行",
|
||||
"ok": "确定",
|
||||
"openNewIssue": "打开新问题",
|
||||
"overwrite": "覆盖",
|
||||
@@ -785,13 +786,13 @@
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切换模型库侧边栏",
|
||||
"Toggle Node Library Sidebar": "切换节点库侧边栏",
|
||||
"Toggle Queue Sidebar": "切换队列侧边栏",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle Workflows Sidebar": "切换工作流侧边栏",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
@@ -1191,219 +1192,322 @@
|
||||
"category": {
|
||||
"3D": "3D",
|
||||
"All": "所有模板",
|
||||
"Area Composition": "区域组成",
|
||||
"Audio": "音频",
|
||||
"Area Composition": "区域合成",
|
||||
"Audio": "音频生成",
|
||||
"Basics": "基础",
|
||||
"ComfyUI Examples": "ComfyUI示例",
|
||||
"ControlNet": "ControlNet",
|
||||
"Custom Nodes": "自定义节点",
|
||||
"Flux": "Flux",
|
||||
"Image": "图片",
|
||||
"Image": "图像生成",
|
||||
"Image API": "图像 API",
|
||||
"Upscaling": "放大",
|
||||
"Video": "视频",
|
||||
"LLM API": "LLM API",
|
||||
"Upscaling": "图像放大",
|
||||
"Video": "视频生成",
|
||||
"Video API": "视频 API"
|
||||
},
|
||||
"template": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "混元3D多视图",
|
||||
"hunyuan-3d-turbo": "混元3D Turbo",
|
||||
"hunyuan3d-non-multiview-train": "混元3D",
|
||||
"3d_hunyuan3d_image_to_model": "混元3D 2.0 图生模型",
|
||||
"3d_hunyuan3d_multiview_to_model": "混元3D 2.0 多视图模型生成",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "混元3D 2.0 多视图模型生成 Turbo",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin:图生模型",
|
||||
"api_rodin_multiview_to_model": "Rodin:多视图模型生成",
|
||||
"api_tripo_image_to_model": "Tripo:图生模型",
|
||||
"api_tripo_multiview_to_model": "Tripo:多视图模型生成",
|
||||
"api_tripo_text_to_model": "Tripo:文生模型"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "区域构成",
|
||||
"area_composition_reversed": "反向区域构成",
|
||||
"area_composition_square_area_for_subject": "主题的方形区域构成"
|
||||
"area_composition": "区域条件控制",
|
||||
"area_composition_square_area_for_subject": "区域件控制生成"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "Stable Audio"
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 歌曲风格/歌词编辑",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1 文生器乐",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1 文生歌曲",
|
||||
"audio_stable_audio_example": "Stable Audio"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "图像生成",
|
||||
"embedding_example": "嵌入",
|
||||
"default": "文生图(默认)",
|
||||
"embedding_example": "文本嵌入模型",
|
||||
"gligen_textbox_example": "Gligen文本框",
|
||||
"image2image": "图像到图像",
|
||||
"inpain_model_outpainting": "Inpaint模型Outpainting",
|
||||
"inpaint_example": "Inpaint",
|
||||
"lora": "Lora",
|
||||
"lora_multiple": "Lora多个"
|
||||
"image2image": "图生图",
|
||||
"inpaint_example": "局部重绘",
|
||||
"inpaint_model_outpainting": "图像外扩",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "多重LoRA"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "双通道姿势处理",
|
||||
"controlnet_example": "ControlNet",
|
||||
"depth_controlnet": "深度ControlNet",
|
||||
"depth_t2i_adapter": "深度T2I适配器",
|
||||
"mixing_controlnets": "混合ControlNets"
|
||||
"2_pass_pose_worship": "姿态 ControlNet 二次采样",
|
||||
"controlnet_example": "涂鸦 ControlNet",
|
||||
"depth_controlnet": "深度 ControlNet",
|
||||
"depth_t2i_adapter": "深度 T2I 适配器",
|
||||
"mixing_controlnets": "混合 ControlNet"
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "Flux Canny Model",
|
||||
"flux_depth_lora_example": "Flux Depth Lora",
|
||||
"flux_dev_checkpoint_example": "Flux Dev的检查点示例",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_depth_lora_example": "Flux Depth LoRA",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8 文生图",
|
||||
"flux_dev_full_text_to_image": "Flux Dev 全量文生图",
|
||||
"flux_fill_inpaint_example": "Flux 局部重绘",
|
||||
"flux_fill_outpaint_example": "Flux 扩图",
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev(基础)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev(组)",
|
||||
"flux_redux_model_example": "Flux Redux Model",
|
||||
"flux_schnell": "Flux Schnell"
|
||||
"flux_schnell": "Flux Schnell fp8 文生图",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell 全量文生图"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 Full",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full",
|
||||
"image_chroma_text_to_image": "Chroma 文生图",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B 文生图",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth",
|
||||
"image_omnigen2_image_edit": "OmniGen2 图像编辑",
|
||||
"image_omnigen2_t2i": "OmniGen2 文生图",
|
||||
"sd3_5_large_blur": "SD3.5 Large 模糊",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Large Canny 控制网",
|
||||
"sd3_5_large_depth": "SD3.5 Large 深度",
|
||||
"sd3_5_simple_example": "SD3.5 简易示例",
|
||||
"sd3_5_simple_example": "SD3.5 简单版本",
|
||||
"sdxl_refiner_prompt_example": "SDXL Refiner提示",
|
||||
"sdxl_revision_text_prompts": "SDXL修订文本提示",
|
||||
"sdxl_revision_zero_positive": "SDXL修订零正",
|
||||
"sdxl_simple_example": "SDXL简单",
|
||||
"sdxl_simple_example": "SDXL简单版本",
|
||||
"sdxlturbo_example": "SDXL Turbo"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 局部修复",
|
||||
"api-openai-dall-e-2-t2i": "Dall-E 2 文生图",
|
||||
"api-openai-dall-e-3-t2i": "Dall-E 3 文生图",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra 文生图",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 文生图",
|
||||
"api_luma_photon_i2i": "Luma Photon 图生图",
|
||||
"api_luma_photon_style_ref": "Luma Photon 风格参考",
|
||||
"api_openai_image_1_i2i": "OpenAI Image-1 图生图",
|
||||
"api_openai_image_1_inpaint": "OpenAI Image-1 局部修复",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI Image-1 多输入",
|
||||
"api_openai_image_1_t2i": "OpenAI Image-1 文生图",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft 颜色控制图像生成",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft 风格控制图像生成",
|
||||
"api_recraft_vector_gen": "Recraft 矢量生成",
|
||||
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra 文生图"
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext Max",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext 多图输入",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext Pro",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]:文生图",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3:文生图",
|
||||
"api_luma_photon_i2i": "Luma Photon:图生图",
|
||||
"api_luma_photon_style_ref": "Luma Photon:风格参考",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI:Dall-E 2 局部修复",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI:Dall-E 2 文生图",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI:Dall-E 3 文生图",
|
||||
"api_openai_image_1_i2i": "OpenAI:GPT-Image-1 图生图",
|
||||
"api_openai_image_1_inpaint": "OpenAI:GPT-Image-1 局部重绘",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI:GPT-Image-1 多输入",
|
||||
"api_openai_image_1_t2i": "OpenAI:GPT-Image-1 文生图",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft:颜色控制图像生成",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft:风格控制图像生成",
|
||||
"api_recraft_vector_gen": "Recraft:矢量生成",
|
||||
"api_runway_reference_to_image": "Runway:参考图到图像",
|
||||
"api_runway_text_to_image": "Runway:文生图",
|
||||
"api_stability_ai_i2i": "Stability AI:图生图",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI:SD3.5 图生图",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI:SD3.5 文生图",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI:Stable Image Ultra 文生图"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "Google Gemini:对话",
|
||||
"api_openai_chat": "OpenAI:对话"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "ESRGAN",
|
||||
"hiresfix_esrgan_workflow": "HiresFix ESRGAN工作流",
|
||||
"hiresfix_latent_workflow": "HiresFix潜在工作流",
|
||||
"latent_upscale_different_prompt_model": "潜在升级不同提示模型"
|
||||
"hiresfix_esrgan_workflow": "HiresFix ESRGAN放大",
|
||||
"hiresfix_latent_workflow": "HiresFix 潜空间放大",
|
||||
"latent_upscale_different_prompt_model": "潜空间放大二次采样"
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "混元视频文本到视频",
|
||||
"image_to_video": "图像到视频",
|
||||
"image_to_video_wan": "Wan 2.1 图像到视频",
|
||||
"ltxv_image_to_video": "LTXV图像到视频",
|
||||
"ltxv_text_to_video": "LTXV文本到视频",
|
||||
"mochi_text_to_video_example": "Mochi文本到视频",
|
||||
"text_to_video_wan": "Wan 2.1 文字到视频",
|
||||
"txt_to_image_to_video": "文本到图像到视频",
|
||||
"hunyuan_video_text_to_video": "混元视频文生视频",
|
||||
"image_to_video": "图生视频",
|
||||
"image_to_video_wan": "Wan 2.1 图生视频",
|
||||
"ltxv_image_to_video": "LTXV图生视频",
|
||||
"ltxv_text_to_video": "LTXV文生视频",
|
||||
"mochi_text_to_video_example": "Mochi文生视频",
|
||||
"text_to_video_wan": "Wan 2.1 文生视频",
|
||||
"txt_to_image_to_video": "文生图转视频",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE 参考视频生成",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE 文生视频",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE 控制视频",
|
||||
"video_wan_vace_flf2v": "Wan VACE 首尾帧",
|
||||
"video_wan_vace_inpainting": "Wan VACE Inpainting",
|
||||
"video_wan_vace_outpainting": "Wan VACE 视频外扩",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet",
|
||||
"wan2_1_fun_inp": "Wan 2.1 图像修复"
|
||||
"wan2_1_fun_inp": "Wan 2.1 局部重绘"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "MiniMax 图生视频",
|
||||
"api_kling_i2v": "Kling 图生视频",
|
||||
"api_luma_i2v": "Luma 图生视频",
|
||||
"api_pika_scene": "Pika 场景:图生视频",
|
||||
"api_pixverse_t2v": "PixVerse 文生视频",
|
||||
"api_hailuo_minimax_i2v": "MiniMax:图生视频",
|
||||
"api_hailuo_minimax_t2v": "MiniMax:文生视频",
|
||||
"api_kling_effects": "Kling:视频特效",
|
||||
"api_kling_flf": "Kling:首尾帧",
|
||||
"api_kling_i2v": "Kling:图生视频",
|
||||
"api_luma_i2v": "Luma:图生视频",
|
||||
"api_luma_t2v": "Luma:文生视频",
|
||||
"api_moonvalley_image_to_video": "Moonvalley:图生视频",
|
||||
"api_moonvalley_text_to_video": "Moonvalley:文生视频",
|
||||
"api_pika_i2v": "Pika:图生视频",
|
||||
"api_pika_scene": "Pika 场景:多图视频生成",
|
||||
"api_pixverse_i2v": "PixVerse:图生视频",
|
||||
"api_pixverse_t2v": "PixVerse:文本到视频",
|
||||
"api_pixverse_template_i2v": "PixVerse特效:图生视频",
|
||||
"api_veo2_i2v": "Veo2 图生视频"
|
||||
"api_runway_first_last_frame": "Runway:首尾帧视频sheng c",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway:Gen3a Turbo 图生视频",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway:Gen4 Turbo 图生视频",
|
||||
"api_veo2_i2v": "Veo2:图生视频"
|
||||
}
|
||||
},
|
||||
"templateDescription": {
|
||||
"3D": {
|
||||
"hunyuan-3d-multiview-elf": "使用 Hunyuan3D 2mv 从多视角生成模型。",
|
||||
"hunyuan-3d-turbo": "使用 Hunyuan3D 2mv turbo 从多视角生成模型。",
|
||||
"hunyuan3d-non-multiview-train": "使用 Hunyuan3D 2.0 从单视角生成模型。",
|
||||
"3d_hunyuan3d_image_to_model": "使用 Hunyuan3D 2.0 单图生成 3D 模型。",
|
||||
"3d_hunyuan3d_multiview_to_model": "使用 Hunyuan3D 2.0 MV 多视角生成 3D 模型。",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "使用 Hunyuan3D 2.0 MV Turbo 多视角生成 3D 模型。",
|
||||
"stable_zero123_example": "通过单张图像生成 3D 视图。"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin AI 单张照片生成高细节 3D 资产。",
|
||||
"api_rodin_multiview_to_model": "Rodin 多视图重建完整 3D 资产。",
|
||||
"api_tripo_image_to_model": "Tripo 2D 图像生成专业的 3D 资产。",
|
||||
"api_tripo_multiview_to_model": "Tripo 多视角生成 3D 资产。",
|
||||
"api_tripo_text_to_model": "Tripo 文本描述生成 3D 物体。"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "通过区域控制图像构图。",
|
||||
"area_composition_reversed": "反向区域构图流程。",
|
||||
"area_composition_square_area_for_subject": "实现主体位置一致性。"
|
||||
"area_composition_square_area_for_subject": "通过区域构成实现主体位置一致性。"
|
||||
},
|
||||
"Audio": {
|
||||
"stable_audio_example": "根据文本描述生成音频。"
|
||||
"audio_ace_step_1_m2m_editing": "使用 ACE-Step v1 M2M 编辑现有歌曲风格和歌词。",
|
||||
"audio_ace_step_1_t2a_instrumentals": "使用 ACE-Step v1 根据文本生成器乐音乐。",
|
||||
"audio_ace_step_1_t2a_song": "使用 ACE-Step v1 根据文本生成带人声的歌曲,支持多语言和风格定制。",
|
||||
"audio_stable_audio_example": "根据文本描述生成音频。"
|
||||
},
|
||||
"Basics": {
|
||||
"default": "根据文本描述生成图像。",
|
||||
"embedding_example": "使用文本反演实现风格一致性。",
|
||||
"gligen_textbox_example": "指定物体的位置和大小。",
|
||||
"embedding_example": "使用文本反演模型实现风格一致性。",
|
||||
"gligen_textbox_example": "通过文本框精确控制物体位置生成图像。",
|
||||
"image2image": "使用文本提示转换现有图像。",
|
||||
"inpain_model_outpainting": "将图像扩展到原始边界之外。",
|
||||
"inpaint_example": "无缝编辑图像的特定部分。",
|
||||
"inpaint_model_outpainting": "将图像扩展到原始边界之外。",
|
||||
"lora": "应用 LoRA 模型以实现特定风格或主题。",
|
||||
"lora_multiple": "组合多个 LoRA 模型以获得独特效果。"
|
||||
},
|
||||
"ControlNet": {
|
||||
"2_pass_pose_worship": "通过姿态参考生成图像。",
|
||||
"controlnet_example": "通过参考图像控制图像生成。",
|
||||
"depth_controlnet": "生成深度感知图像。",
|
||||
"controlnet_example": "通过涂鸦参考图像引导生成。",
|
||||
"depth_controlnet": "通过深度信息引导生成图像。",
|
||||
"depth_t2i_adapter": "使用 T2I 适配器快速生成深度感知图像。",
|
||||
"mixing_controlnets": "组合多个 ControlNet 模型。"
|
||||
"mixing_controlnets": "组合多个 ControlNet 模型生成图像。"
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "通过边缘检测生成图像。",
|
||||
"flux_depth_lora_example": "使用深度感知 LoRA 生成图像。",
|
||||
"flux_dev_checkpoint_example": "使用 flux 开发模型生成图像。",
|
||||
"flux_fill_inpaint_example": "填充图像中缺失的部分。",
|
||||
"flux_fill_outpaint_example": "使用 flux 外扩生成图像。",
|
||||
"flux_redux_model_example": "将参考图像的风格迁移到引导图像生成(flux)。",
|
||||
"flux_schnell": "使用 flux schnell 快速生成图像。"
|
||||
"flux_canny_model_example": "通过边缘检测引导 Flux 生成图像。",
|
||||
"flux_depth_lora_example": "通过深度信息引导 Flux LoRA 生成图像。",
|
||||
"flux_dev_checkpoint_example": "使用 Flux Dev fp8 量化版生成图像,适合显存有限设备,仅需一个模型文件,画质略低于完整版。",
|
||||
"flux_dev_full_text_to_image": "使用 Flux Dev 完整版生成高质量图像,需要更大显存和多个模型文件,提示遵循性和画质最佳。",
|
||||
"flux_fill_inpaint_example": "使用 Flux 修复图像缺失区域。",
|
||||
"flux_fill_outpaint_example": "使用 Flux 将图像外扩生成更大尺寸的图片",
|
||||
"flux_kontext_dev_basic": "使用 Flux Kontext(基础版)编辑图像,适合了解节点组合",
|
||||
"flux_kontext_dev_grouped": "使用 Flux Kontext 组节点版本,简洁封装版适合快速复用",
|
||||
"flux_redux_model_example": "通过参考图像风格迁移,使用 Flux Redux 生成新图像。",
|
||||
"flux_schnell": "使用 Flux Schnell fp8 量化版快速生成图像,适合低端硬件,仅需4步即可生成。",
|
||||
"flux_schnell_full_text_to_image": "使用 Flux Schnell 完整版快速生成图像,Apache2.0 许可,仅需4步,兼顾速度和画质。"
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "使用 HiDream E1 编辑图像。",
|
||||
"hidream_i1_dev": "使用 HiDream I1 Dev 生成图像。",
|
||||
"hidream_i1_fast": "使用 HiDream I1 快速生成图像。",
|
||||
"hidream_i1_full": "使用 HiDream I1 生成图像。",
|
||||
"sd3_5_large_blur": "使用 SD 3.5 通过模糊参考图像生成图像。",
|
||||
"sd3_5_large_canny_controlnet_example": "使用边缘检测引导 SD 3.5 图像生成。",
|
||||
"sd3_5_large_depth": "使用 SD 3.5 生成深度感知图像。",
|
||||
"hidream_e1_full": "HiDream E1 - 专业级自然语言图像编辑模型。",
|
||||
"hidream_i1_dev": "HiDream I1 Dev - 28 步推理,适合中端硬件,平衡速度与质量。",
|
||||
"hidream_i1_fast": "HiDream I1 Fast - 16 步推理,适合低端硬件快速预览。",
|
||||
"hidream_i1_full": "HiDream I1 Full - 50 步推理,画质最佳。",
|
||||
"image_chroma_text_to_image": "Chroma 基于 Flux 改进,架构有所变化。",
|
||||
"image_cosmos_predict2_2B_t2i": "使用 Cosmos-Predict2 2B T2I 生成物理真实、高保真、细节丰富的图像。",
|
||||
"image_lotus_depth_v1_1": "在 ComfyUI 中运行 Lotus Depth,实现高效、细节丰富的单目深度估计。",
|
||||
"image_omnigen2_image_edit": "使用 OmniGen2 高级图像编辑和文本渲染能力,通过自然语言指令编辑图片。",
|
||||
"image_omnigen2_t2i": "使用 OmniGen2 统一 7B 多模态模型和双路径架构,根据文本生成高质量图像。",
|
||||
"sd3_5_large_blur": "使用 SD 3.5 通过模糊参考图像引导生成图像。",
|
||||
"sd3_5_large_canny_controlnet_example": "使用 SD 3.5 Canny ControlNet 通过边缘检测引导生成图像。",
|
||||
"sd3_5_large_depth": "使用 SD 3.5 通过深度信息引导生成图像。",
|
||||
"sd3_5_simple_example": "使用 SD 3.5 生成图像。",
|
||||
"sdxl_refiner_prompt_example": "使用精炼器提升 SDXL 输出效果。",
|
||||
"sdxl_revision_text_prompts": "将参考图像的概念迁移到 SDXL 图像生成中。",
|
||||
"sdxl_revision_zero_positive": "在 SDXL 图像生成中结合文本提示和参考图像。",
|
||||
"sdxl_refiner_prompt_example": "使用 Refiner 模型提升 SDXL 图像质量。",
|
||||
"sdxl_revision_text_prompts": "通过参考图像概念迁移,使用 SDXL Revision 生成图像。",
|
||||
"sdxl_revision_zero_positive": "结合文本提示和参考图像,使用 SDXL Revision 生成图像。",
|
||||
"sdxl_simple_example": "使用 SDXL 生成高质量图像。",
|
||||
"sdxlturbo_example": "使用 SDXL Turbo 一步生成图像。"
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "使用 Dall-E 2 API 对图像进行修复。",
|
||||
"api-openai-dall-e-2-t2i": "使用 Dall-E 2 API 根据文本描述生成图像。",
|
||||
"api-openai-dall-e-3-t2i": "使用 Dall-E 3 API 根据文本描述生成图像。",
|
||||
"api_bfl_flux_pro_t2i": "使用 FLUX.1 [pro] 生成高质量、细节丰富、提示遵循性强且多样化的图像。",
|
||||
"api_ideogram_v3_t2i": "生成高质量图像与提示对齐、照片级真实感和文本渲染。可用于专业级 logo、宣传海报、落地页概念、产品摄影等。轻松打造复杂空间构图、精细背景、精准光影与色彩、逼真环境细节。",
|
||||
"api_luma_photon_i2i": "结合图像和提示词引导图像生成。",
|
||||
"api_luma_photon_style_ref": "精确控制并融合风格参考。Luma Photon 捕捉每个参考图像的精髓,让你在保持专业品质的同时融合不同视觉元素。",
|
||||
"api_openai_image_1_i2i": "使用 GPT Image 1 API 通过图像生成图像。",
|
||||
"api_openai_image_1_inpaint": "使用 GPT Image 1 API 对图像进行修复。",
|
||||
"api_openai_image_1_multi_inputs": "使用 GPT Image 1 API 多输入生成图像。",
|
||||
"api_openai_image_1_t2i": "使用 GPT Image 1 API 根据文本描述生成图像。",
|
||||
"api_recraft_image_gen_with_color_control": "创建自定义调色板以复用或为每张照片手动选色。匹配品牌色彩,打造专属视觉风格。",
|
||||
"api_recraft_image_gen_with_style_control": "通过视觉示例控制风格、对齐位置、微调物体。存储并分享风格,实现品牌一致性。",
|
||||
"api_recraft_vector_gen": "通过文本提示生成矢量图像,使用 Recraft 的 AI 矢量生成器。可用于 logo、海报、图标集、广告、横幅和模型。生成高质量 SVG 文件,几秒内为你的应用或网站创建品牌矢量插画。",
|
||||
"api_stability_sd3_t2i": "生成高质量、提示遵循性极佳的图像。适用于专业场景,分辨率达 1 兆像素。"
|
||||
"api_bfl_flux_1_kontext_max_image": "使用 Flux.1 Kontext max image 编辑图像。",
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "多图输入,使用 Flux.1 Kontext 编辑。",
|
||||
"api_bfl_flux_1_kontext_pro_image": "使用 Flux.1 Kontext pro image 编辑图像。",
|
||||
"api_bfl_flux_pro_t2i": "使用 FLUX.1 Pro 生成高质量、提示遵循性强的图像。",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3 高质量、提示对齐、照片级真实感和文本渲染。",
|
||||
"api_luma_photon_i2i": "结合图像和提示词引导生成。",
|
||||
"api_luma_photon_style_ref": "融合风格参考,精确控制,保持专业品质。",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI Dall-E 2 API 局部修复。",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI Dall-E 2 API 文本生成图像。",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI Dall-E 3 API 文本生成图像。",
|
||||
"api_openai_image_1_i2i": "OpenAI GPT Image 1 API 图生图。",
|
||||
"api_openai_image_1_inpaint": "OpenAI GPT Image 1 API 局部修复。",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI GPT Image 1 API 多输入生成图像。",
|
||||
"api_openai_image_1_t2i": "OpenAI GPT Image 1 API 文本生成图像。",
|
||||
"api_recraft_image_gen_with_color_control": "自定义调色板,打造品牌专属视觉风格。",
|
||||
"api_recraft_image_gen_with_style_control": "通过视觉示例控制风格、对齐位置、微调物体,实现品牌一致性。",
|
||||
"api_recraft_vector_gen": "通过文本生成高质量矢量图像,适用于 logo、海报等。",
|
||||
"api_runway_reference_to_image": "Runway AI 参考风格和构图生成新图像。",
|
||||
"api_runway_text_to_image": "使用 Runway AI 文本生成高质量图像。",
|
||||
"api_stability_ai_i2i": "Stability AI 高质量图生图,适合专业编辑和风格迁移。",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI SD3.5 图生图,分辨率达 1 兆像素。",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI SD3.5 文本生成高质量图像,分辨率达 1 兆像素。",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "生成高质量、提示遵循性极佳的图像,适合专业场景,分辨率达 1 兆像素。"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_google_gemini": "体验 Google Gemini 多模态推理能力。",
|
||||
"api_openai_chat": "与 OpenAI 高级语言模型智能对话。"
|
||||
},
|
||||
"Upscaling": {
|
||||
"esrgan_example": "使用超分模型提升图像质量。",
|
||||
"hiresfix_esrgan_workflow": "在中间步骤使用超分模型提升图像质量。",
|
||||
"hiresfix_latent_workflow": "在 latent 空间中提升图像质量。",
|
||||
"esrgan_example": "使用 ESRGAN 超分模型提升图像质量。",
|
||||
"hiresfix_esrgan_workflow": "在中间步骤使用 ESRGAN 超分提升图像质量。",
|
||||
"hiresfix_latent_workflow": "在潜空间提升图像质量。",
|
||||
"latent_upscale_different_prompt_model": "放大图像并在不同阶段更换提示词。"
|
||||
},
|
||||
"Video": {
|
||||
"hunyuan_video_text_to_video": "使用 Hunyuan 模型生成视频。",
|
||||
"image_to_video": "将图像转换为动画视频。",
|
||||
"image_to_video_wan": "快速将图像生成视频。",
|
||||
"ltxv_image_to_video": "将静态图像转换为视频。",
|
||||
"ltxv_text_to_video": "根据文本描述生成视频。",
|
||||
"mochi_text_to_video_example": "使用 Mochi 模型生成视频。",
|
||||
"text_to_video_wan": "快速将文本描述生成视频。",
|
||||
"txt_to_image_to_video": "先由文本生成图像,再转换为视频。",
|
||||
"wan2_1_flf2v_720_f16": "通过控制首帧和尾帧生成视频。",
|
||||
"wan2_1_fun_control": "通过姿态、深度、边缘等控制引导视频生成。",
|
||||
"wan2_1_fun_inp": "通过起始帧和结束帧生成视频。"
|
||||
"hunyuan_video_text_to_video": "Hunyuan 文本转视频。",
|
||||
"image_to_video": "静态图像转视频。",
|
||||
"image_to_video_wan": "Wan 2.1 图像转视频。",
|
||||
"ltxv_image_to_video": "LTXV 静态图像转视频。",
|
||||
"ltxv_text_to_video": "LTXV 文本转视频。",
|
||||
"mochi_text_to_video_example": "Mochi 文本转视频。",
|
||||
"text_to_video_wan": "Wan 2.1 文本转视频。",
|
||||
"txt_to_image_to_video": "先生成图像再转为视频。",
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "使用 Cosmos-Predict2 2B Video2World 生成物理真实、高保真、一致性强的视频。",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B,支持高级镜头控制。",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B,生成动态镜头运动视频。",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE 参考图风格一致性视频生成。",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE-14B 文本转视频,支持 480p 和 720p。",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE 控制输入视频和参考图生成新视频。",
|
||||
"video_wan_vace_flf2v": "Wan VACE 首尾帧,支持自定义关键帧序列,实现平滑过渡。",
|
||||
"video_wan_vace_inpainting": "Wan VACE 局部编辑,保留周围内容,适合物体移除或替换。",
|
||||
"video_wan_vace_outpainting": "Wan VACE 外扩,扩展视频尺寸。",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V,控制首尾帧生成 720p 视频。",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet,姿态、深度、边缘等控制视频生成。",
|
||||
"wan2_1_fun_inp": "Wan 2.1 起始帧和结束帧生成视频。"
|
||||
},
|
||||
"Video API": {
|
||||
"api_hailuo_minimax_i2v": "通过图像和文本生成精致视频,支持 CGI 效果和流行 AI 拥抱等特效。多种视频风格和主题,满足你的创意需求。",
|
||||
"api_kling_i2v": "生成动作、表情、镜头运动等提示遵循性强的视频。支持复杂提示和顺序动作,让你成为场景导演。",
|
||||
"api_luma_i2v": "将静态图像瞬间转化为高质量动画。",
|
||||
"api_pika_scene": "将多张图像作为素材,生成融合所有内容的视频。",
|
||||
"api_pixverse_t2v": "根据提示生成高还原度、动态效果出色的视频。",
|
||||
"api_pixverse_template_i2v": "将静态图像转化为带有动态和特效的视频。",
|
||||
"api_veo2_i2v": "使用 Google Veo2 API 通过图像生成视频。"
|
||||
"api_hailuo_minimax_i2v": "MiniMax 图像+文本生成精致视频,支持 CGI 效果。",
|
||||
"api_hailuo_minimax_t2v": "MiniMax 文本生成高质量视频,支持 CGI 效果和多样风格。",
|
||||
"api_kling_effects": "Kling 应用视觉特效生成动态视频。",
|
||||
"api_kling_flf": "Kling 控制首尾帧生成视频。",
|
||||
"api_kling_i2v": "Kling 高提示遵循性,支持动作、表情、镜头运动等。",
|
||||
"api_luma_i2v": "Luma 静态图像一键生成高质量动画。",
|
||||
"api_luma_t2v": "Luma 简单提示生成高质量视频。",
|
||||
"api_moonvalley_image_to_video": "Moonvalley 图像生成 1080p 电影级视频,训练数据全部为授权内容。",
|
||||
"api_moonvalley_text_to_video": "Moonvalley 文本生成 1080p 电影级视频,训练数据全部为授权内容。",
|
||||
"api_pika_i2v": "Pika AI 单张图像生成流畅动画视频。",
|
||||
"api_pika_scene": "Pika Scenes 多图融合生成视频。",
|
||||
"api_pixverse_i2v": "PixVerse 静态图像生成动态特效视频。",
|
||||
"api_pixverse_t2v": "PixVerse 高还原度、动态效果出色的视频生成。",
|
||||
"api_pixverse_template_i2v": "PixVerse 静态图像生成动态特效视频。",
|
||||
"api_runway_first_last_frame": "Runway 精准控制首尾帧生成平滑过渡视频。",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway Gen3a Turbo 静态图像生成电影级视频。",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway Gen4 Turbo 图像生成动态视频。",
|
||||
"api_veo2_i2v": "Google Veo2 API 图像生成视频。"
|
||||
}
|
||||
},
|
||||
"title": "从模板开始"
|
||||
|
||||
@@ -326,7 +326,8 @@
|
||||
"Bottom": "底部",
|
||||
"Disabled": "禁用",
|
||||
"Top": "顶部"
|
||||
}
|
||||
},
|
||||
"tooltip": "選單列位置。在行動裝置上,選單始終顯示於頂端。"
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "校验节点定义(慢)",
|
||||
@@ -409,4 +410,4 @@
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "始终吸附到网格"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,22 @@ const zProgressWsMessage = z.object({
|
||||
node: zNodeId
|
||||
})
|
||||
|
||||
const zNodeProgressState = z.object({
|
||||
value: z.number(),
|
||||
max: z.number(),
|
||||
state: z.enum(['pending', 'running', 'finished', 'error']),
|
||||
node_id: zNodeId,
|
||||
prompt_id: zPromptId,
|
||||
display_node_id: zNodeId.optional(),
|
||||
parent_node_id: zNodeId.optional(),
|
||||
real_node_id: zNodeId.optional()
|
||||
})
|
||||
|
||||
const zProgressStateWsMessage = z.object({
|
||||
prompt_id: zPromptId,
|
||||
nodes: z.record(zNodeId, zNodeProgressState)
|
||||
})
|
||||
|
||||
const zExecutingWsMessage = z.object({
|
||||
node: zNodeId,
|
||||
display_node: zNodeId,
|
||||
@@ -113,6 +129,8 @@ const zLogRawResponse = z.object({
|
||||
entries: z.array(zLogEntry)
|
||||
})
|
||||
|
||||
const zFeatureFlagsWsMessage = z.record(z.string(), z.any())
|
||||
|
||||
export type StatusWsMessageStatus = z.infer<typeof zStatusWsMessageStatus>
|
||||
export type StatusWsMessage = z.infer<typeof zStatusWsMessage>
|
||||
export type ProgressWsMessage = z.infer<typeof zProgressWsMessage>
|
||||
@@ -132,6 +150,9 @@ export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
|
||||
export type DisplayComponentWsMessage = z.infer<
|
||||
typeof zDisplayComponentWsMessage
|
||||
>
|
||||
export type NodeProgressState = z.infer<typeof zNodeProgressState>
|
||||
export type ProgressStateWsMessage = z.infer<typeof zProgressStateWsMessage>
|
||||
export type FeatureFlagsWsMessage = z.infer<typeof zFeatureFlagsWsMessage>
|
||||
// End of ws messages
|
||||
|
||||
const zPromptInputItem = z.object({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
|
||||
import type {
|
||||
DisplayComponentWsMessage,
|
||||
EmbeddingsResponse,
|
||||
@@ -11,10 +12,12 @@ import type {
|
||||
ExecutionStartWsMessage,
|
||||
ExecutionSuccessWsMessage,
|
||||
ExtensionsResponse,
|
||||
FeatureFlagsWsMessage,
|
||||
HistoryTaskItem,
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
PendingTaskItem,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
PromptResponse,
|
||||
@@ -103,8 +106,19 @@ interface BackendApiCalls {
|
||||
logs: LogsWsMessage
|
||||
/** Binary preview/progress data */
|
||||
b_preview: Blob
|
||||
/** Binary preview with metadata (node_id, prompt_id) */
|
||||
b_preview_with_metadata: {
|
||||
blob: Blob
|
||||
nodeId: string
|
||||
parentNodeId: string
|
||||
displayNodeId: string
|
||||
realNodeId: string
|
||||
promptId: string
|
||||
}
|
||||
progress_text: ProgressTextWsMessage
|
||||
progress_state: ProgressStateWsMessage
|
||||
display_component: DisplayComponentWsMessage
|
||||
feature_flags: FeatureFlagsWsMessage
|
||||
}
|
||||
|
||||
/** Dictionary of all api calls */
|
||||
@@ -234,6 +248,19 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
reportedUnknownMessageTypes = new Set<string>()
|
||||
|
||||
/**
|
||||
* Get feature flags supported by this frontend client.
|
||||
* Returns a copy to prevent external modification.
|
||||
*/
|
||||
getClientFeatureFlags(): Record<string, unknown> {
|
||||
return { ...defaultClientFeatureFlags }
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature flags received from the backend server.
|
||||
*/
|
||||
serverFeatureFlags: Record<string, unknown> = {}
|
||||
|
||||
/**
|
||||
* The auth token for the comfy org account if the user is logged in.
|
||||
* This is only used for {@link queuePrompt} now. It is not directly
|
||||
@@ -375,6 +402,15 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
this.socket.addEventListener('open', () => {
|
||||
opened = true
|
||||
|
||||
// Send feature flags as the first message
|
||||
this.socket!.send(
|
||||
JSON.stringify({
|
||||
type: 'feature_flags',
|
||||
data: this.getClientFeatureFlags()
|
||||
})
|
||||
)
|
||||
|
||||
if (isReconnect) {
|
||||
this.dispatchCustomEvent('reconnected')
|
||||
}
|
||||
@@ -432,6 +468,33 @@ export class ComfyApi extends EventTarget {
|
||||
})
|
||||
this.dispatchCustomEvent('b_preview', imageBlob)
|
||||
break
|
||||
case 4:
|
||||
// PREVIEW_IMAGE_WITH_METADATA
|
||||
const decoder4 = new TextDecoder()
|
||||
const metadataLength = view.getUint32(4)
|
||||
const metadataBytes = event.data.slice(8, 8 + metadataLength)
|
||||
const metadata = JSON.parse(decoder4.decode(metadataBytes))
|
||||
const imageData4 = event.data.slice(8 + metadataLength)
|
||||
|
||||
let imageMime4 = metadata.image_type
|
||||
|
||||
const imageBlob4 = new Blob([imageData4], {
|
||||
type: imageMime4
|
||||
})
|
||||
|
||||
// Dispatch enhanced preview event with metadata
|
||||
this.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob: imageBlob4,
|
||||
nodeId: metadata.node_id,
|
||||
displayNodeId: metadata.display_node_id,
|
||||
parentNodeId: metadata.parent_node_id,
|
||||
realNodeId: metadata.real_node_id,
|
||||
promptId: metadata.prompt_id
|
||||
})
|
||||
|
||||
// Also dispatch legacy b_preview for backward compatibility
|
||||
this.dispatchCustomEvent('b_preview', imageBlob4)
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown binary websocket message of type ${eventType}`
|
||||
@@ -461,6 +524,7 @@ export class ComfyApi extends EventTarget {
|
||||
case 'execution_cached':
|
||||
case 'execution_success':
|
||||
case 'progress':
|
||||
case 'progress_state':
|
||||
case 'executed':
|
||||
case 'graphChanged':
|
||||
case 'promptQueued':
|
||||
@@ -468,6 +532,14 @@ export class ComfyApi extends EventTarget {
|
||||
case 'b_preview':
|
||||
this.dispatchCustomEvent(msg.type, msg.data)
|
||||
break
|
||||
case 'feature_flags':
|
||||
// Store server feature flags
|
||||
this.serverFeatureFlags = msg.data
|
||||
console.log(
|
||||
'Server feature flags received:',
|
||||
this.serverFeatureFlags
|
||||
)
|
||||
break
|
||||
default:
|
||||
if (this.#registered.has(msg.type)) {
|
||||
// Fallback for custom types - calls super direct.
|
||||
@@ -689,7 +761,8 @@ export class ComfyApi extends EventTarget {
|
||||
Running: data.queue_running.map((prompt: Record<number, any>) => ({
|
||||
taskType: 'Running',
|
||||
prompt,
|
||||
remove: { name: 'Cancel', cb: () => api.interrupt() }
|
||||
// prompt[1] is the prompt id
|
||||
remove: { name: 'Cancel', cb: () => api.interrupt(prompt[1]) }
|
||||
})),
|
||||
Pending: data.queue_pending.map((prompt: Record<number, any>) => ({
|
||||
taskType: 'Pending',
|
||||
@@ -770,10 +843,15 @@ export class ComfyApi extends EventTarget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupts the execution of the running prompt
|
||||
* Interrupts the execution of the running prompt. If runningPromptId is provided,
|
||||
* it is included in the payload as a helpful hint to the backend.
|
||||
* @param {string | null} [runningPromptId] Optional Running Prompt ID to interrupt
|
||||
*/
|
||||
async interrupt() {
|
||||
await this.#postItem('interrupt', null)
|
||||
async interrupt(runningPromptId: string | null) {
|
||||
await this.#postItem(
|
||||
'interrupt',
|
||||
runningPromptId ? { prompt_id: runningPromptId } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -956,6 +1034,33 @@ export class ComfyApi extends EventTarget {
|
||||
async getCustomNodesI18n(): Promise<Record<string, any>> {
|
||||
return (await axios.get(this.apiURL('/i18n'))).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server supports a specific feature.
|
||||
* @param featureName The name of the feature to check
|
||||
* @returns true if the feature is supported, false otherwise
|
||||
*/
|
||||
serverSupportsFeature(featureName: string): boolean {
|
||||
return this.serverFeatureFlags[featureName] === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a server feature flag value.
|
||||
* @param featureName The name of the feature to get
|
||||
* @param defaultValue The default value if the feature is not found
|
||||
* @returns The feature value or default
|
||||
*/
|
||||
getServerFeature<T = unknown>(featureName: string, defaultValue?: T): T {
|
||||
return (this.serverFeatureFlags[featureName] ?? defaultValue) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all server feature flags.
|
||||
* @returns Copy of all server feature flags
|
||||
*/
|
||||
getServerFeatures(): Record<string, unknown> {
|
||||
return { ...this.serverFeatureFlags }
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ComfyApi()
|
||||
|
||||
@@ -194,6 +194,8 @@ export class ComfyApp {
|
||||
|
||||
/**
|
||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||
* TODO: Update to support multiple executing nodes. This getter returns only the first executing node.
|
||||
* Consider updating consumers to handle multiple nodes or use executingNodeIds array.
|
||||
*/
|
||||
get runningNodeId(): NodeId | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
@@ -635,10 +637,6 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('executing', () => {
|
||||
this.graph.setDirtyCanvas(true, false)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.revokePreviews(this.runningNodeId)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete this.nodePreviewImages[this.runningNodeId]
|
||||
})
|
||||
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
@@ -689,15 +687,13 @@ export class ComfyApp {
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview', ({ detail }) => {
|
||||
const id = this.runningNodeId
|
||||
if (id == null) return
|
||||
|
||||
const blob = detail
|
||||
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
|
||||
// Enhanced preview with explicit node context
|
||||
const { blob, displayNodeId } = detail
|
||||
this.revokePreviews(displayNodeId)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
// Ensure clean up if `executing` event is missed.
|
||||
this.revokePreviews(id)
|
||||
this.nodePreviewImages[id] = [blobUrl]
|
||||
// Preview cleanup is now handled in progress_state event to support multiple concurrent previews
|
||||
this.nodePreviewImages[displayNodeId] = [blobUrl]
|
||||
})
|
||||
|
||||
api.init()
|
||||
|
||||
@@ -237,6 +237,7 @@ export class ComponentWidgetImpl<
|
||||
component: Component
|
||||
inputSpec: InputSpec
|
||||
props?: P
|
||||
componentProps?: Partial<P>
|
||||
options: DOMWidgetOptions<V>
|
||||
}) {
|
||||
super({
|
||||
@@ -245,7 +246,9 @@ export class ComponentWidgetImpl<
|
||||
})
|
||||
this.component = obj.component
|
||||
this.inputSpec = obj.inputSpec
|
||||
this.props = obj.props
|
||||
this.props = obj.componentProps
|
||||
? ({ ...obj.props, ...obj.componentProps } as P)
|
||||
: obj.props
|
||||
}
|
||||
|
||||
override computeLayoutSize() {
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { $el } from '@/scripts/ui'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
@@ -107,7 +108,11 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
if (this.id == app.runningNodeId) {
|
||||
const nodeId = String(this.id)
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId)
|
||||
const state =
|
||||
useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state
|
||||
if (state === 'running') {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
@@ -362,7 +367,11 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
if (this.id == app.runningNodeId) {
|
||||
const nodeId = String(this.id)
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId)
|
||||
const state =
|
||||
useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state
|
||||
if (state === 'running') {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
NodeError,
|
||||
NodeProgressState,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
@@ -21,6 +23,9 @@ import type {
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { useCanvasStore } from './graphStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
|
||||
@@ -46,7 +51,97 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||
const executingNodeId = ref<NodeId | null>(null)
|
||||
// This is the progress of all nodes in the currently executing workflow
|
||||
const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
|
||||
|
||||
/**
|
||||
* Convert execution context node IDs to NodeLocatorIds
|
||||
* @param nodeId The node ID from execution context (could be execution ID)
|
||||
* @returns The NodeLocatorId
|
||||
*/
|
||||
const executionIdToNodeLocatorId = (
|
||||
nodeId: string | number
|
||||
): NodeLocatorId => {
|
||||
const nodeIdStr = String(nodeId)
|
||||
|
||||
if (!nodeIdStr.includes(':')) {
|
||||
// It's a top-level node ID
|
||||
return nodeIdStr as NodeLocatorId
|
||||
}
|
||||
|
||||
// It's an execution node ID
|
||||
const parts = nodeIdStr.split(':')
|
||||
const localNodeId = parts[parts.length - 1]
|
||||
const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts)
|
||||
const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId)
|
||||
return nodeLocatorId
|
||||
}
|
||||
|
||||
const mergeExecutionProgressStates = (
|
||||
currentState: NodeProgressState | undefined,
|
||||
newState: NodeProgressState
|
||||
): NodeProgressState => {
|
||||
if (currentState === undefined) {
|
||||
return newState
|
||||
}
|
||||
|
||||
const mergedState = { ...currentState }
|
||||
if (mergedState.state === 'error') {
|
||||
return mergedState
|
||||
} else if (newState.state === 'running') {
|
||||
const newPerc = newState.max > 0 ? newState.value / newState.max : 0.0
|
||||
const oldPerc =
|
||||
mergedState.max > 0 ? mergedState.value / mergedState.max : 0.0
|
||||
if (
|
||||
mergedState.state !== 'running' ||
|
||||
oldPerc === 0.0 ||
|
||||
newPerc < oldPerc
|
||||
) {
|
||||
mergedState.value = newState.value
|
||||
mergedState.max = newState.max
|
||||
}
|
||||
mergedState.state = 'running'
|
||||
}
|
||||
|
||||
return mergedState
|
||||
}
|
||||
|
||||
const nodeLocationProgressStates = computed<
|
||||
Record<NodeLocatorId, NodeProgressState>
|
||||
>(() => {
|
||||
const result: Record<NodeLocatorId, NodeProgressState> = {}
|
||||
|
||||
const states = nodeProgressStates.value // Apparently doing this inside `Object.entries` causes issues
|
||||
for (const [_, state] of Object.entries(states)) {
|
||||
const parts = String(state.display_node_id).split(':')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const executionId = parts.slice(0, i + 1).join(':')
|
||||
const locatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!locatorId) continue
|
||||
|
||||
result[locatorId] = mergeExecutionProgressStates(
|
||||
result[locatorId],
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// Easily access all currently executing node IDs
|
||||
const executingNodeIds = computed<NodeId[]>(() => {
|
||||
return Object.entries(nodeProgressStates)
|
||||
.filter(([_, state]) => state.state === 'running')
|
||||
.map(([nodeId, _]) => nodeId)
|
||||
})
|
||||
|
||||
// @deprecated For backward compatibility - stores the primary executing node ID
|
||||
const executingNodeId = computed<NodeId | null>(() => {
|
||||
return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null
|
||||
})
|
||||
|
||||
// For backward compatibility - returns the primary executing node
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
@@ -93,30 +188,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||
}
|
||||
|
||||
const executionIdToCurrentId = (id: string) => {
|
||||
const subgraph = workflowStore.activeSubgraph
|
||||
|
||||
// Short-circuit: ID belongs to the parent workflow / no active subgraph
|
||||
if (!id.includes(':')) {
|
||||
return !subgraph ? id : undefined
|
||||
} else if (!subgraph) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||
const subgraphNodeIds = id.split(':')
|
||||
|
||||
// If the last subgraph is the active subgraph, return the node ID
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
subgraph.rootGraph,
|
||||
subgraphNodeIds
|
||||
)
|
||||
if (subgraphs.at(-1) === subgraph) {
|
||||
return subgraphNodeIds.at(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the progress of the currently executing node, if any
|
||||
// This is the progress of the currently executing node (for backward compatibility)
|
||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||
const executingNodeProgress = computed(() =>
|
||||
_executingNodeProgress.value
|
||||
@@ -153,6 +225,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
api.addEventListener('executing', handleExecuting)
|
||||
api.addEventListener('progress', handleProgress)
|
||||
api.addEventListener('progress_state', handleProgressState)
|
||||
api.addEventListener('status', handleStatus)
|
||||
api.addEventListener('execution_error', handleExecutionError)
|
||||
}
|
||||
@@ -165,6 +238,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.removeEventListener('executed', handleExecuted)
|
||||
api.removeEventListener('executing', handleExecuting)
|
||||
api.removeEventListener('progress', handleProgress)
|
||||
api.removeEventListener('progress_state', handleProgressState)
|
||||
api.removeEventListener('status', handleStatus)
|
||||
api.removeEventListener('execution_error', handleExecutionError)
|
||||
api.removeEventListener('progress_text', handleProgressText)
|
||||
@@ -194,19 +268,42 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
if (!activePrompt.value) return
|
||||
|
||||
if (executingNodeId.value && activePrompt.value) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
activePrompt.value.nodes[executingNodeId.value] = true
|
||||
// Update the executing nodes list
|
||||
if (typeof e.detail !== 'string') {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
activePromptId.value = null
|
||||
}
|
||||
if (typeof e.detail === 'string') {
|
||||
executingNodeId.value = executionIdToCurrentId(e.detail) ?? null
|
||||
} else {
|
||||
executingNodeId.value = e.detail
|
||||
if (executingNodeId.value === null) {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
activePromptId.value = null
|
||||
}
|
||||
|
||||
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) {
|
||||
const { nodes } = e.detail
|
||||
|
||||
// Revoke previews for nodes that are starting to execute
|
||||
for (const nodeId in nodes) {
|
||||
const nodeState = nodes[nodeId]
|
||||
if (nodeState.state === 'running' && !nodeProgressStates.value[nodeId]) {
|
||||
// This node just started executing, revoke its previews
|
||||
// Note that we're doing the *actual* node id instead of the display node id
|
||||
// here intentionally. That way, we don't clear the preview every time a new node
|
||||
// within an expanded graph starts executing.
|
||||
app.revokePreviews(nodeId)
|
||||
delete app.nodePreviewImages[nodeId]
|
||||
}
|
||||
}
|
||||
|
||||
// Update the progress states for all nodes
|
||||
nodeProgressStates.value = nodes
|
||||
|
||||
// If we have progress for the currently executing node, update it for backwards compatibility
|
||||
if (executingNodeId.value && nodes[executingNodeId.value]) {
|
||||
const nodeState = nodes[executingNodeId.value]
|
||||
_executingNodeProgress.value = {
|
||||
value: nodeState.value,
|
||||
max: nodeState.max,
|
||||
prompt_id: nodeState.prompt_id,
|
||||
node: nodeState.display_node_id || nodeState.node_id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +336,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const { nodeId, text } = e.detail
|
||||
if (!text || !nodeId) return
|
||||
|
||||
// Handle hierarchical node IDs for subgraphs
|
||||
// Handle execution node IDs for subgraphs
|
||||
const currentId = getNodeIdIfExecuting(nodeId)
|
||||
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||
if (!node) return
|
||||
@@ -250,7 +347,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
function handleDisplayComponent(e: CustomEvent<DisplayComponentWsMessage>) {
|
||||
const { node_id: nodeId, component, props = {} } = e.detail
|
||||
|
||||
// Handle hierarchical node IDs for subgraphs
|
||||
// Handle execution node IDs for subgraphs
|
||||
const currentId = getNodeIdIfExecuting(nodeId)
|
||||
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||
if (!node) return
|
||||
@@ -290,6 +387,18 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a NodeLocatorId to an execution context ID
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @returns The execution ID or null if conversion fails
|
||||
*/
|
||||
const nodeLocatorIdToExecutionId = (
|
||||
locatorId: NodeLocatorId | string
|
||||
): string | null => {
|
||||
const executionId = workflowStore.nodeLocatorIdToNodeExecutionId(locatorId)
|
||||
return executionId
|
||||
}
|
||||
|
||||
return {
|
||||
isIdle,
|
||||
clientId,
|
||||
@@ -310,9 +419,13 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
lastExecutionError,
|
||||
/**
|
||||
* The id of the node that is currently being executed
|
||||
* The id of the node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
executingNodeId,
|
||||
/**
|
||||
* The list of all nodes that are currently executing
|
||||
*/
|
||||
executingNodeIds,
|
||||
/**
|
||||
* The prompt that is currently being executed
|
||||
*/
|
||||
@@ -330,17 +443,25 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
executionProgress,
|
||||
/**
|
||||
* The node that is currently being executed
|
||||
* The node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
executingNode,
|
||||
/**
|
||||
* The progress of the executing node (if the node reports progress)
|
||||
* The progress of the executing node (backward compatibility)
|
||||
*/
|
||||
executingNodeProgress,
|
||||
/**
|
||||
* All node progress states from progress_state events
|
||||
*/
|
||||
nodeProgressStates,
|
||||
nodeLocationProgressStates,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt,
|
||||
// Raw executing progress data for backward compatibility in ComfyApp.
|
||||
_executingNodeProgress
|
||||
_executingNodeProgress,
|
||||
// NodeLocatorId conversion helpers
|
||||
executionIdToNodeLocatorId,
|
||||
nodeLocatorIdToExecutionId
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,10 +4,18 @@ import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
@@ -163,6 +171,15 @@ export interface WorkflowStore {
|
||||
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
|
||||
updateActiveGraph: () => void
|
||||
executionIdToCurrentId: (id: string) => any
|
||||
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
|
||||
nodeExecutionIdToNodeLocatorId: (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
) => NodeLocatorId | null
|
||||
nodeLocatorIdToNodeId: (locatorId: NodeLocatorId | string) => NodeId | null
|
||||
nodeLocatorIdToNodeExecutionId: (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
) => NodeExecutionId | null
|
||||
}
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
@@ -473,7 +490,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||
// Parse the execution ID (e.g., "123:456:789")
|
||||
const subgraphNodeIds = id.split(':')
|
||||
|
||||
// Start from the root graph
|
||||
@@ -488,6 +505,136 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
|
||||
watch(activeWorkflow, updateActiveGraph)
|
||||
|
||||
/**
|
||||
* Convert a node ID to a NodeLocatorId
|
||||
* @param nodeId The local node ID
|
||||
* @param subgraph The subgraph containing the node (defaults to active subgraph)
|
||||
* @returns The NodeLocatorId (for root graph nodes, returns the node ID as-is)
|
||||
*/
|
||||
const nodeIdToNodeLocatorId = (
|
||||
nodeId: NodeId,
|
||||
subgraph?: Subgraph
|
||||
): NodeLocatorId => {
|
||||
const targetSubgraph = subgraph ?? activeSubgraph.value
|
||||
if (!targetSubgraph) {
|
||||
// Node is in the root graph, return the node ID as-is
|
||||
return String(nodeId) as NodeLocatorId
|
||||
}
|
||||
|
||||
return createNodeLocatorId(targetSubgraph.id, nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an execution ID to a NodeLocatorId
|
||||
* @param nodeExecutionId The execution node ID (e.g., "123:456:789")
|
||||
* @returns The NodeLocatorId or null if conversion fails
|
||||
*/
|
||||
const nodeExecutionIdToNodeLocatorId = (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
): NodeLocatorId | null => {
|
||||
// Handle simple node IDs (root graph - no colons)
|
||||
if (!nodeExecutionId.includes(':')) {
|
||||
return nodeExecutionId as NodeLocatorId
|
||||
}
|
||||
|
||||
const parts = parseNodeExecutionId(nodeExecutionId)
|
||||
if (!parts || parts.length === 0) return null
|
||||
|
||||
const nodeId = parts[parts.length - 1]
|
||||
const subgraphNodeIds = parts.slice(0, -1)
|
||||
|
||||
if (subgraphNodeIds.length === 0) {
|
||||
// Node is in root graph, return the node ID as-is
|
||||
return String(nodeId) as NodeLocatorId
|
||||
}
|
||||
|
||||
try {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
subgraphNodeIds.map((id) => String(id))
|
||||
)
|
||||
const immediateSubgraph = subgraphs[subgraphs.length - 1]
|
||||
return createNodeLocatorId(immediateSubgraph.id, nodeId)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the node ID from a NodeLocatorId
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @returns The local node ID or null if invalid
|
||||
*/
|
||||
const nodeLocatorIdToNodeId = (
|
||||
locatorId: NodeLocatorId | string
|
||||
): NodeId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
return parsed?.localNodeId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a NodeLocatorId to an execution ID for a specific context
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @param targetSubgraph The subgraph context (defaults to active subgraph)
|
||||
* @returns The execution ID or null if the node is not accessible from the target context
|
||||
*/
|
||||
const nodeLocatorIdToNodeExecutionId = (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
): NodeExecutionId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
if (!parsed) return null
|
||||
|
||||
const { subgraphUuid, localNodeId } = parsed
|
||||
|
||||
// If no subgraph UUID, this is a root graph node
|
||||
if (!subgraphUuid) {
|
||||
return String(localNodeId) as NodeExecutionId
|
||||
}
|
||||
|
||||
// Find the path from root to the subgraph with this UUID
|
||||
const findSubgraphPath = (
|
||||
graph: LGraph | Subgraph,
|
||||
targetUuid: string,
|
||||
path: NodeId[] = []
|
||||
): NodeId[] | null => {
|
||||
if (isSubgraph(graph) && graph.id === targetUuid) {
|
||||
return path
|
||||
}
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (node.isSubgraphNode?.() && (node as any).subgraph) {
|
||||
const result = findSubgraphPath((node as any).subgraph, targetUuid, [
|
||||
...path,
|
||||
node.id
|
||||
])
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const path = findSubgraphPath(comfyApp.graph, subgraphUuid)
|
||||
if (!path) return null
|
||||
|
||||
// If we have a target subgraph, check if the path goes through it
|
||||
if (
|
||||
targetSubgraph &&
|
||||
!path.some((_, idx) => {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
path.slice(0, idx + 1).map((id) => String(id))
|
||||
)
|
||||
return subgraphs[subgraphs.length - 1] === targetSubgraph
|
||||
})
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createNodeExecutionId([...path, localNodeId])
|
||||
}
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
isActive,
|
||||
@@ -514,7 +661,11 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
isSubgraphActive,
|
||||
activeSubgraph,
|
||||
updateActiveGraph,
|
||||
executionIdToCurrentId
|
||||
executionIdToCurrentId,
|
||||
nodeIdToNodeLocatorId,
|
||||
nodeExecutionIdToNodeLocatorId,
|
||||
nodeLocatorIdToNodeId,
|
||||
nodeLocatorIdToNodeExecutionId
|
||||
}
|
||||
}) satisfies () => WorkflowStore
|
||||
|
||||
|
||||
@@ -31,6 +31,16 @@ export type { ComfyApi } from '@/scripts/api'
|
||||
export type { ComfyApp } from '@/scripts/app'
|
||||
export type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
export type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
export type {
|
||||
NodeLocatorId,
|
||||
NodeExecutionId,
|
||||
isNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
parseNodeLocatorId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
createNodeExecutionId
|
||||
} from './nodeIdentification'
|
||||
export type {
|
||||
EmbeddingsResponse,
|
||||
ExtensionsResponse,
|
||||
|
||||
6
src/types/litegraph-augmentation.d.ts
vendored
@@ -69,7 +69,7 @@ declare module '@comfyorg/litegraph/dist/interfaces' {
|
||||
* ComfyUI extensions of litegraph
|
||||
*/
|
||||
declare module '@comfyorg/litegraph' {
|
||||
import type { ExecutableLGraphNode } from '@comfyorg/litegraph'
|
||||
import type { ExecutableLGraphNode, ExecutionId } from '@comfyorg/litegraph'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
||||
@@ -99,8 +99,10 @@ declare module '@comfyorg/litegraph' {
|
||||
setInnerNodes?(nodes: LGraphNode[]): void
|
||||
/** Originally a group node API. */
|
||||
getInnerNodes?(
|
||||
nodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>,
|
||||
subgraphNodePath?: readonly NodeId[],
|
||||
nodes?: ExecutableLGraphNode[],
|
||||
subgraphs?: WeakSet<LGraphNode>
|
||||
subgraphs?: Set<LGraphNode>
|
||||
): ExecutableLGraphNode[]
|
||||
/** @deprecated groupNode */
|
||||
convertToNodes?(): LGraphNode[]
|
||||
|
||||
123
src/types/nodeIdentification.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
/**
|
||||
* A globally unique identifier for nodes that maintains consistency across
|
||||
* multiple instances of the same subgraph.
|
||||
*
|
||||
* Format:
|
||||
* - For subgraph nodes: `<immediate-contained-subgraph-uuid>:<local-node-id>`
|
||||
* - For root graph nodes: `<local-node-id>`
|
||||
*
|
||||
* Examples:
|
||||
* - "a1b2c3d4-e5f6-7890-abcd-ef1234567890:123" (node in subgraph)
|
||||
* - "456" (node in root graph)
|
||||
*
|
||||
* Unlike execution IDs which change based on the instance path,
|
||||
* NodeLocatorId remains the same for all instances of a particular node.
|
||||
*/
|
||||
export type NodeLocatorId = string
|
||||
|
||||
/**
|
||||
* An execution identifier representing a node's position in nested subgraphs.
|
||||
* Also known as ExecutionId in some contexts.
|
||||
*
|
||||
* Format: Colon-separated path of node IDs
|
||||
* Example: "123:456:789" (node 789 in subgraph 456 in subgraph 123)
|
||||
*/
|
||||
export type NodeExecutionId = string
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a NodeLocatorId
|
||||
*/
|
||||
export function isNodeLocatorId(value: unknown): value is NodeLocatorId {
|
||||
if (typeof value !== 'string') return false
|
||||
|
||||
// Check if it's a simple node ID (root graph node)
|
||||
const parts = value.split(':')
|
||||
if (parts.length === 1) {
|
||||
// Simple node ID - must be non-empty
|
||||
return value.length > 0
|
||||
}
|
||||
|
||||
// Check for UUID:nodeId format
|
||||
if (parts.length !== 2) return false
|
||||
|
||||
// Check that node ID part is not empty
|
||||
if (!parts[1]) return false
|
||||
|
||||
// Basic UUID format check (8-4-4-4-12 hex characters)
|
||||
const uuidPattern =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
return uuidPattern.test(parts[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a NodeExecutionId
|
||||
*/
|
||||
export function isNodeExecutionId(value: unknown): value is NodeExecutionId {
|
||||
if (typeof value !== 'string') return false
|
||||
// Must contain at least one colon to be an execution ID
|
||||
return value.includes(':')
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a NodeLocatorId into its components
|
||||
* @param id The NodeLocatorId to parse
|
||||
* @returns The subgraph UUID and local node ID, or null if invalid
|
||||
*/
|
||||
export function parseNodeLocatorId(
|
||||
id: string
|
||||
): { subgraphUuid: string | null; localNodeId: NodeId } | null {
|
||||
if (!isNodeLocatorId(id)) return null
|
||||
|
||||
const parts = id.split(':')
|
||||
|
||||
if (parts.length === 1) {
|
||||
// Simple node ID (root graph)
|
||||
return {
|
||||
subgraphUuid: null,
|
||||
localNodeId: isNaN(Number(id)) ? id : Number(id)
|
||||
}
|
||||
}
|
||||
|
||||
const [subgraphUuid, localNodeId] = parts
|
||||
return {
|
||||
subgraphUuid,
|
||||
localNodeId: isNaN(Number(localNodeId)) ? localNodeId : Number(localNodeId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a NodeLocatorId from components
|
||||
* @param subgraphUuid The UUID of the immediate containing subgraph
|
||||
* @param localNodeId The local node ID within that subgraph
|
||||
* @returns A properly formatted NodeLocatorId
|
||||
*/
|
||||
export function createNodeLocatorId(
|
||||
subgraphUuid: string,
|
||||
localNodeId: NodeId
|
||||
): NodeLocatorId {
|
||||
return `${subgraphUuid}:${localNodeId}` as NodeLocatorId
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a NodeExecutionId into its component node IDs
|
||||
* @param id The NodeExecutionId to parse
|
||||
* @returns Array of node IDs from root to target, or null if not an execution ID
|
||||
*/
|
||||
export function parseNodeExecutionId(id: string): NodeId[] | null {
|
||||
if (!isNodeExecutionId(id)) return null
|
||||
|
||||
return id
|
||||
.split(':')
|
||||
.map((part) => (isNaN(Number(part)) ? part : Number(part)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a NodeExecutionId from an array of node IDs
|
||||
* @param nodeIds Array of node IDs from root to target
|
||||
* @returns A properly formatted NodeExecutionId
|
||||
*/
|
||||
export function createNodeExecutionId(nodeIds: NodeId[]): NodeExecutionId {
|
||||
return nodeIds.join(':') as NodeExecutionId
|
||||
}
|
||||
53
src/utils/executableGroupNodeChildDTO.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
type ExecutableLGraphNode,
|
||||
ExecutableNodeDTO,
|
||||
type ExecutionId,
|
||||
type LGraphNode,
|
||||
type NodeId,
|
||||
type SubgraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
import type { GroupNodeHandler } from '@/extensions/core/groupNode'
|
||||
|
||||
export class ExecutableGroupNodeChildDTO extends ExecutableNodeDTO {
|
||||
groupNodeHandler?: GroupNodeHandler
|
||||
|
||||
constructor(
|
||||
/** The actual node that this DTO wraps. */
|
||||
node: LGraphNode | SubgraphNode,
|
||||
/** A list of subgraph instance node IDs from the root graph to the containing instance. @see {@link id} */
|
||||
subgraphNodePath: readonly NodeId[],
|
||||
/** A flattened map of all DTOs in this node network. Subgraph instances have been expanded into their inner nodes. */
|
||||
nodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>,
|
||||
/** The actual subgraph instance that contains this node, otherise undefined. */
|
||||
subgraphNode?: SubgraphNode | undefined,
|
||||
groupNodeHandler?: GroupNodeHandler
|
||||
) {
|
||||
super(node, subgraphNodePath, nodesByExecutionId, subgraphNode)
|
||||
this.groupNodeHandler = groupNodeHandler
|
||||
}
|
||||
|
||||
override resolveInput(slot: number) {
|
||||
const inputNode = this.node.getInputNode(slot)
|
||||
if (!inputNode) return
|
||||
|
||||
const link = this.node.getInputLink(slot)
|
||||
if (!link) throw new Error('Failed to get input link')
|
||||
|
||||
const id = String(inputNode.id).split(':').at(-1)
|
||||
if (id === undefined) throw new Error('Invalid input node id')
|
||||
|
||||
const inputNodeDto = this.nodesByExecutionId?.get(id)
|
||||
if (!inputNodeDto) {
|
||||
throw new Error(
|
||||
`Failed to get input node ${id} for group node child ${this.id} with slot ${slot}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
node: inputNodeDto,
|
||||
origin_id: String(inputNode.id),
|
||||
origin_slot: link.origin_slot
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/utils/executableGroupNodeDto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
type ExecutableLGraphNode,
|
||||
ExecutableNodeDTO,
|
||||
type ISlotType,
|
||||
LGraphEventMode,
|
||||
type LGraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
export const GROUP = Symbol()
|
||||
|
||||
export function isGroupNode(node: LGraphNode): boolean {
|
||||
return node.constructor?.nodeData?.[GROUP] !== undefined
|
||||
}
|
||||
|
||||
export class ExecutableGroupNodeDTO extends ExecutableNodeDTO {
|
||||
override get isVirtualNode(): true {
|
||||
return true
|
||||
}
|
||||
|
||||
override getInnerNodes(): ExecutableLGraphNode[] {
|
||||
return this.node.getInnerNodes?.(this.nodesByExecutionId) ?? []
|
||||
}
|
||||
|
||||
override resolveOutput(slot: number, type: ISlotType, visited: Set<string>) {
|
||||
// Temporary duplication: Bypass nodes are bypassed using the first input with matching type
|
||||
if (this.mode === LGraphEventMode.BYPASS) {
|
||||
const { inputs } = this
|
||||
|
||||
// Bypass nodes by finding first input with matching type
|
||||
const parentInputIndexes = Object.keys(inputs).map(Number)
|
||||
// Prioritise exact slot index
|
||||
const indexes = [slot, ...parentInputIndexes]
|
||||
const matchingIndex = indexes.find((i) => inputs[i]?.type === type)
|
||||
|
||||
// No input types match
|
||||
if (matchingIndex === undefined) return
|
||||
|
||||
return this.resolveInput(matchingIndex, visited)
|
||||
}
|
||||
|
||||
const linkId = this.node.outputs[slot]?.links?.at(0)
|
||||
const link = this.node.graph?.getLink(linkId)
|
||||
if (!link) {
|
||||
throw new Error(
|
||||
`Failed to get link for group node ${this.node.id} with link ${linkId}`
|
||||
)
|
||||
}
|
||||
|
||||
const updated = this.node.updateLink?.(link)
|
||||
if (!updated) {
|
||||
throw new Error(
|
||||
`Failed to update link for group node ${this.node.id} with link ${linkId}`
|
||||
)
|
||||
}
|
||||
|
||||
const node = this.node
|
||||
.getInnerNodes?.(this.nodesByExecutionId)
|
||||
.find((node) => node.id === updated.origin_id)
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Failed to get node for group node ${this.node.id} with link ${linkId}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
origin_id: `${this.id}:${(updated.origin_id as string).split(':').at(-1)}`,
|
||||
origin_slot: updated.origin_slot
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { LGraph, NodeId } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
ExecutableLGraphNode,
|
||||
ExecutionId,
|
||||
LGraph,
|
||||
NodeId
|
||||
} from '@comfyorg/litegraph'
|
||||
import {
|
||||
ExecutableNodeDTO,
|
||||
LGraphEventMode,
|
||||
@@ -10,6 +15,7 @@ import type {
|
||||
ComfyWorkflowJSON
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
import { ExecutableGroupNodeDTO, isGroupNode } from './executableGroupNodeDto'
|
||||
import { compressWidgetInputSlots } from './litegraphUtil'
|
||||
|
||||
/**
|
||||
@@ -54,7 +60,9 @@ export const graphToPrompt = async (
|
||||
const { sortNodes = false, queueNodeIds } = options
|
||||
|
||||
for (const node of graph.computeExecutionOrder(false)) {
|
||||
const innerNodes = node.getInnerNodes ? node.getInnerNodes() : [node]
|
||||
const innerNodes = node.getInnerNodes
|
||||
? node.getInnerNodes(new Map())
|
||||
: [node]
|
||||
for (const innerNode of innerNodes) {
|
||||
if (innerNode.isVirtualNode) {
|
||||
innerNode.applyToGraph?.()
|
||||
@@ -78,82 +86,87 @@ export const graphToPrompt = async (
|
||||
workflow.extra ??= {}
|
||||
workflow.extra.frontendVersion = __COMFYUI_FRONTEND_VERSION__
|
||||
|
||||
const computedNodeDtos = graph
|
||||
.computeExecutionOrder(false)
|
||||
.map(
|
||||
(node) =>
|
||||
new ExecutableNodeDTO(
|
||||
const nodeDtoMap = new Map<ExecutionId, ExecutableLGraphNode>()
|
||||
for (const node of graph.computeExecutionOrder(false)) {
|
||||
const dto: ExecutableLGraphNode = isGroupNode(node)
|
||||
? new ExecutableGroupNodeDTO(node, [], nodeDtoMap)
|
||||
: new ExecutableNodeDTO(
|
||||
node,
|
||||
[],
|
||||
nodeDtoMap,
|
||||
node instanceof SubgraphNode ? node : undefined
|
||||
)
|
||||
)
|
||||
|
||||
for (const innerNode of dto.getInnerNodes()) {
|
||||
nodeDtoMap.set(innerNode.id, innerNode)
|
||||
}
|
||||
|
||||
nodeDtoMap.set(dto.id, dto)
|
||||
}
|
||||
|
||||
let output: ComfyApiWorkflow = {}
|
||||
// Process nodes in order of execution
|
||||
for (const outerNode of computedNodeDtos) {
|
||||
for (const node of nodeDtoMap.values()) {
|
||||
// Don't serialize muted nodes
|
||||
if (
|
||||
outerNode.mode === LGraphEventMode.NEVER ||
|
||||
outerNode.mode === LGraphEventMode.BYPASS
|
||||
node.isVirtualNode ||
|
||||
node.mode === LGraphEventMode.NEVER ||
|
||||
node.mode === LGraphEventMode.BYPASS
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const node of outerNode.getInnerNodes()) {
|
||||
if (
|
||||
node.isVirtualNode ||
|
||||
node.mode === LGraphEventMode.NEVER ||
|
||||
node.mode === LGraphEventMode.BYPASS
|
||||
) {
|
||||
const inputs: ComfyApiWorkflow[string]['inputs'] = {}
|
||||
const { widgets } = node
|
||||
|
||||
// Store all widget values
|
||||
if (widgets) {
|
||||
for (const [i, widget] of widgets.entries()) {
|
||||
if (!widget.name || widget.options?.serialize === false) continue
|
||||
|
||||
const widgetValue = widget.serializeValue
|
||||
? await widget.serializeValue(node, i)
|
||||
: widget.value
|
||||
// By default, Array values are reserved to represent node connections.
|
||||
// We need to wrap the array as an object to avoid the misinterpretation
|
||||
// of the array as a node connection.
|
||||
// The backend automatically unwraps the object to an array during
|
||||
// execution.
|
||||
inputs[widget.name] = Array.isArray(widgetValue)
|
||||
? {
|
||||
__value__: widgetValue
|
||||
}
|
||||
: widgetValue
|
||||
}
|
||||
}
|
||||
|
||||
// Store all node links
|
||||
for (const [i, input] of node.inputs.entries()) {
|
||||
const resolvedInput = node.resolveInput(i)
|
||||
if (!resolvedInput) continue
|
||||
|
||||
// Resolved to an actual widget value rather than a node connection
|
||||
if (resolvedInput.widgetInfo) {
|
||||
const { value } = resolvedInput.widgetInfo
|
||||
inputs[input.name] = Array.isArray(value) ? { __value__: value } : value
|
||||
continue
|
||||
}
|
||||
|
||||
const inputs: ComfyApiWorkflow[string]['inputs'] = {}
|
||||
const { widgets } = node
|
||||
inputs[input.name] = [
|
||||
String(resolvedInput.origin_id),
|
||||
// @ts-expect-error link.origin_slot is already number.
|
||||
parseInt(resolvedInput.origin_slot)
|
||||
]
|
||||
}
|
||||
|
||||
// Store all widget values
|
||||
if (widgets) {
|
||||
for (const [i, widget] of widgets.entries()) {
|
||||
if (!widget.name || widget.options?.serialize === false) continue
|
||||
|
||||
const widgetValue = widget.serializeValue
|
||||
? await widget.serializeValue(node, i)
|
||||
: widget.value
|
||||
// By default, Array values are reserved to represent node connections.
|
||||
// We need to wrap the array as an object to avoid the misinterpretation
|
||||
// of the array as a node connection.
|
||||
// The backend automatically unwraps the object to an array during
|
||||
// execution.
|
||||
inputs[widget.name] = Array.isArray(widgetValue)
|
||||
? {
|
||||
__value__: widgetValue
|
||||
}
|
||||
: widgetValue
|
||||
}
|
||||
}
|
||||
|
||||
// Store all node links
|
||||
for (const [i, input] of node.inputs.entries()) {
|
||||
const resolvedInput = node.resolveInput(i)
|
||||
if (!resolvedInput) continue
|
||||
|
||||
inputs[input.name] = [
|
||||
String(resolvedInput.origin_id),
|
||||
// @ts-expect-error link.origin_slot is already number.
|
||||
parseInt(resolvedInput.origin_slot)
|
||||
]
|
||||
}
|
||||
|
||||
output[String(node.id)] = {
|
||||
inputs,
|
||||
// TODO(huchenlei): Filter out all nodes that cannot be mapped to a
|
||||
// comfyClass.
|
||||
class_type: node.comfyClass!,
|
||||
// Ignored by the backend.
|
||||
_meta: {
|
||||
title: node.title
|
||||
}
|
||||
output[String(node.id)] = {
|
||||
inputs,
|
||||
// TODO(huchenlei): Filter out all nodes that cannot be mapped to a
|
||||
// comfyClass.
|
||||
class_type: node.comfyClass!,
|
||||
// Ignored by the backend.
|
||||
_meta: {
|
||||
title: node.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="comfyui-body grid h-full w-full overflow-hidden">
|
||||
<div id="comfyui-body-top" class="comfyui-body-top">
|
||||
<TopMenubar v-if="useNewMenu === 'Top'" />
|
||||
<TopMenubar v-if="showTopMenu" />
|
||||
</div>
|
||||
<div id="comfyui-body-bottom" class="comfyui-body-bottom">
|
||||
<TopMenubar v-if="useNewMenu === 'Bottom'" />
|
||||
<TopMenubar v-if="showBottomMenu" />
|
||||
</div>
|
||||
<div id="comfyui-body-left" class="comfyui-body-left" />
|
||||
<div id="comfyui-body-right" class="comfyui-body-right" />
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { useBreakpoints, useEventListener } from '@vueuse/core'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onBeforeUnmount, onMounted, watch, watchEffect } from 'vue'
|
||||
@@ -70,6 +70,12 @@ const settingStore = useSettingStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const queueStore = useQueueStore()
|
||||
const breakpoints = useBreakpoints({ md: 961 })
|
||||
const isMobile = breakpoints.smaller('md')
|
||||
const showTopMenu = computed(() => isMobile.value || useNewMenu.value === 'Top')
|
||||
const showBottomMenu = computed(
|
||||
() => !isMobile.value && useNewMenu.value === 'Bottom'
|
||||
)
|
||||
|
||||
watch(
|
||||
() => colorPaletteStore.completedActivePalette,
|
||||
|
||||
208
tests-ui/tests/api.featureFlags.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
describe('API Feature Flags', () => {
|
||||
let mockWebSocket: any
|
||||
const wsEventHandlers: { [key: string]: (event: any) => void } = {}
|
||||
|
||||
beforeEach(() => {
|
||||
// Use fake timers
|
||||
vi.useFakeTimers()
|
||||
|
||||
// Mock WebSocket
|
||||
mockWebSocket = {
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
addEventListener: vi.fn(
|
||||
(event: string, handler: (event: any) => void) => {
|
||||
wsEventHandlers[event] = handler
|
||||
}
|
||||
),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
|
||||
// Mock WebSocket constructor
|
||||
global.WebSocket = vi.fn().mockImplementation(() => mockWebSocket) as any
|
||||
|
||||
// Reset API state
|
||||
api.serverFeatureFlags = {}
|
||||
|
||||
// Mock getClientFeatureFlags to return test feature flags
|
||||
vi.spyOn(api, 'getClientFeatureFlags').mockReturnValue({
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Feature flags negotiation', () => {
|
||||
it('should send client feature flags as first message on connection', async () => {
|
||||
// Initialize API connection
|
||||
const initPromise = api.init()
|
||||
|
||||
// Simulate connection open
|
||||
wsEventHandlers['open'](new Event('open'))
|
||||
|
||||
// Check that feature flags were sent as first message
|
||||
expect(mockWebSocket.send).toHaveBeenCalledTimes(1)
|
||||
const sentMessage = JSON.parse(mockWebSocket.send.mock.calls[0][0])
|
||||
expect(sentMessage).toEqual({
|
||||
type: 'feature_flags',
|
||||
data: {
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
}
|
||||
})
|
||||
|
||||
// Simulate server response with status message
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: 0 } },
|
||||
sid: 'test-sid'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate server feature flags response
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'feature_flags',
|
||||
data: {
|
||||
supports_preview_metadata: true,
|
||||
async_execution: true,
|
||||
supported_formats: ['webp', 'jpeg', 'png'],
|
||||
api_version: '1.0.0',
|
||||
max_upload_size: 104857600,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await initPromise
|
||||
|
||||
// Check that server features were stored
|
||||
expect(api.serverFeatureFlags).toEqual({
|
||||
supports_preview_metadata: true,
|
||||
async_execution: true,
|
||||
supported_formats: ['webp', 'jpeg', 'png'],
|
||||
api_version: '1.0.0',
|
||||
max_upload_size: 104857600,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle server without feature flags support', async () => {
|
||||
// Initialize API connection
|
||||
const initPromise = api.init()
|
||||
|
||||
// Simulate connection open
|
||||
wsEventHandlers['open'](new Event('open'))
|
||||
|
||||
// Clear the send mock to reset
|
||||
mockWebSocket.send.mockClear()
|
||||
|
||||
// Simulate server response with status but no feature flags
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: 0 } },
|
||||
sid: 'test-sid'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate some other message (not feature flags)
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'execution_start',
|
||||
data: {}
|
||||
})
|
||||
})
|
||||
|
||||
await initPromise
|
||||
|
||||
// Server features should remain empty
|
||||
expect(api.serverFeatureFlags).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Feature checking methods', () => {
|
||||
beforeEach(() => {
|
||||
// Set up some test features
|
||||
api.serverFeatureFlags = {
|
||||
supports_preview_metadata: true,
|
||||
async_execution: false,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
}
|
||||
})
|
||||
|
||||
it('should check if server supports a boolean feature', () => {
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
expect(api.serverSupportsFeature('async_execution')).toBe(false)
|
||||
expect(api.serverSupportsFeature('non_existent_feature')).toBe(false)
|
||||
})
|
||||
|
||||
it('should get server feature value', () => {
|
||||
expect(api.getServerFeature('supports_preview_metadata')).toBe(true)
|
||||
expect(api.getServerFeature('capabilities')).toEqual([
|
||||
'isolated_nodes',
|
||||
'dynamic_models'
|
||||
])
|
||||
expect(api.getServerFeature('non_existent_feature')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Client feature flags configuration', () => {
|
||||
it('should use mocked client feature flags', () => {
|
||||
// Verify mocked flags are returned
|
||||
const clientFlags = api.getClientFeatureFlags()
|
||||
expect(clientFlags).toEqual({
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a copy of client feature flags', () => {
|
||||
// Temporarily restore the real implementation for this test
|
||||
vi.mocked(api.getClientFeatureFlags).mockRestore()
|
||||
|
||||
// Verify that modifications to returned object don't affect original
|
||||
const clientFlags1 = api.getClientFeatureFlags()
|
||||
const clientFlags2 = api.getClientFeatureFlags()
|
||||
|
||||
// Should be different objects
|
||||
expect(clientFlags1).not.toBe(clientFlags2)
|
||||
|
||||
// But with same content
|
||||
expect(clientFlags1).toEqual(clientFlags2)
|
||||
|
||||
// Modifying one should not affect the other
|
||||
clientFlags1.test_flag = true
|
||||
expect(api.getClientFeatureFlags()).not.toHaveProperty('test_flag')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with preview messages', () => {
|
||||
it('should affect preview message handling based on feature support', () => {
|
||||
// Test with metadata support
|
||||
api.serverFeatureFlags = { supports_preview_metadata: true }
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
|
||||
// Test without metadata support
|
||||
api.serverFeatureFlags = {}
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,12 +3,20 @@ import { nextTick, reactive } from 'vue'
|
||||
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
|
||||
// Mock i18n module
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string, fallback: string) =>
|
||||
key === 'g.nodesRunning' ? 'nodes running' : fallback
|
||||
}))
|
||||
|
||||
// Mock the execution store
|
||||
const executionStore = reactive({
|
||||
isIdle: true,
|
||||
executionProgress: 0,
|
||||
executingNode: null as any,
|
||||
executingNodeProgress: 0
|
||||
executingNodeProgress: 0,
|
||||
nodeProgressStates: {} as any,
|
||||
activePrompt: null as any
|
||||
})
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStore
|
||||
@@ -37,6 +45,8 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executionProgress = 0
|
||||
executionStore.executingNode = null as any
|
||||
executionStore.executingNodeProgress = 0
|
||||
executionStore.nodeProgressStates = {}
|
||||
executionStore.activePrompt = null
|
||||
|
||||
// reset setting and workflow stores
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
@@ -97,13 +107,41 @@ describe('useBrowserTabTitle', () => {
|
||||
expect(document.title).toBe('[30%]ComfyUI')
|
||||
})
|
||||
|
||||
it('shows node execution title when executing a node', async () => {
|
||||
it('shows node execution title when executing a node using nodeProgressStates', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
|
||||
}
|
||||
executionStore.activePrompt = {
|
||||
workflow: {
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: [{ id: 1, type: 'Foo' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][50%] Foo')
|
||||
})
|
||||
|
||||
it('shows multiple nodes running when multiple nodes are executing', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': {
|
||||
state: 'running',
|
||||
value: 5,
|
||||
max: 10,
|
||||
node: '1',
|
||||
prompt_id: 'test'
|
||||
},
|
||||
'2': { state: 'running', value: 8, max: 10, node: '2', prompt_id: 'test' }
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][2 nodes running]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
// Mock the workflowStore
|
||||
vi.mock('@/stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => ({
|
||||
nodeExecutionIdToNodeLocatorId: vi.fn(),
|
||||
nodeIdToNodeLocatorId: vi.fn(),
|
||||
nodeLocatorIdToNodeExecutionId: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
// Remove any previous global types
|
||||
declare global {
|
||||
// Empty interface to override any previous declarations
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface Window {}
|
||||
}
|
||||
|
||||
@@ -22,12 +33,16 @@ vi.mock('@/composables/node/useNodeProgressText', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
// Create a local mock instead of using global to avoid conflicts
|
||||
const mockApp = {
|
||||
graph: {
|
||||
getNodeById: vi.fn()
|
||||
// Mock the app import with proper implementation
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
getNodeById: vi.fn()
|
||||
},
|
||||
revokePreviews: vi.fn(),
|
||||
nodePreviewImages: {}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('executionStore - display_component handling', () => {
|
||||
function createDisplayComponentEvent(
|
||||
@@ -47,7 +62,7 @@ describe('executionStore - display_component handling', () => {
|
||||
|
||||
function handleDisplayComponentMessage(event: CustomEvent) {
|
||||
const { node_id, component } = event.detail
|
||||
const node = mockApp.graph.getNodeById(node_id)
|
||||
const node = vi.mocked(app.graph.getNodeById)(node_id)
|
||||
if (node && component === 'ChatHistoryWidget') {
|
||||
mockShowChatHistory(node)
|
||||
}
|
||||
@@ -60,23 +75,121 @@ describe('executionStore - display_component handling', () => {
|
||||
})
|
||||
|
||||
it('handles ChatHistoryWidget display_component messages', () => {
|
||||
const mockNode = { id: '123' }
|
||||
mockApp.graph.getNodeById.mockReturnValue(mockNode)
|
||||
const mockNode = { id: '123' } as any
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode)
|
||||
|
||||
const event = createDisplayComponentEvent('123')
|
||||
handleDisplayComponentMessage(event)
|
||||
|
||||
expect(mockApp.graph.getNodeById).toHaveBeenCalledWith('123')
|
||||
expect(app.graph.getNodeById).toHaveBeenCalledWith('123')
|
||||
expect(mockShowChatHistory).toHaveBeenCalledWith(mockNode)
|
||||
})
|
||||
|
||||
it('does nothing if node is not found', () => {
|
||||
mockApp.graph.getNodeById.mockReturnValue(null)
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(null)
|
||||
|
||||
const event = createDisplayComponentEvent('non-existent')
|
||||
handleDisplayComponentMessage(event)
|
||||
|
||||
expect(mockApp.graph.getNodeById).toHaveBeenCalledWith('non-existent')
|
||||
expect(app.graph.getNodeById).toHaveBeenCalledWith('non-existent')
|
||||
expect(mockShowChatHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionStore - NodeLocatorId conversions', () => {
|
||||
let store: ReturnType<typeof useExecutionStore>
|
||||
let workflowStore: ReturnType<typeof useWorkflowStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Create the mock workflowStore instance
|
||||
const mockWorkflowStore = {
|
||||
nodeExecutionIdToNodeLocatorId: vi.fn(),
|
||||
nodeIdToNodeLocatorId: vi.fn(),
|
||||
nodeLocatorIdToNodeExecutionId: vi.fn()
|
||||
}
|
||||
|
||||
// Mock the useWorkflowStore function to return our mock
|
||||
vi.mocked(useWorkflowStore).mockReturnValue(mockWorkflowStore as any)
|
||||
|
||||
workflowStore = mockWorkflowStore as any
|
||||
store = useExecutionStore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('executionIdToNodeLocatorId', () => {
|
||||
it('should convert execution ID to NodeLocatorId', () => {
|
||||
// Mock subgraph structure
|
||||
const mockSubgraph = {
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
_nodes: []
|
||||
}
|
||||
|
||||
const mockNode = {
|
||||
id: 123,
|
||||
isSubgraphNode: () => true,
|
||||
subgraph: mockSubgraph
|
||||
} as any
|
||||
|
||||
// Mock app.graph.getNodeById to return the mock node
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode)
|
||||
|
||||
const result = store.executionIdToNodeLocatorId('123:456')
|
||||
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should convert simple node ID to NodeLocatorId', () => {
|
||||
const result = store.executionIdToNodeLocatorId('123')
|
||||
|
||||
// For simple node IDs, it should return the ID as-is
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should handle numeric node IDs', () => {
|
||||
const result = store.executionIdToNodeLocatorId(123)
|
||||
|
||||
// For numeric IDs, it should convert to string and return as-is
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null when conversion fails', () => {
|
||||
// Mock app.graph.getNodeById to return null (node not found)
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(null)
|
||||
|
||||
// This should throw an error as the node is not found
|
||||
expect(() => store.executionIdToNodeLocatorId('999:456')).toThrow(
|
||||
'Subgraph not found: 999'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToExecutionId', () => {
|
||||
it('should convert NodeLocatorId to execution ID', () => {
|
||||
const mockExecutionId = '123:456'
|
||||
vi.spyOn(workflowStore, 'nodeLocatorIdToNodeExecutionId').mockReturnValue(
|
||||
mockExecutionId as any
|
||||
)
|
||||
|
||||
const result = store.nodeLocatorIdToExecutionId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
|
||||
expect(workflowStore.nodeLocatorIdToNodeExecutionId).toHaveBeenCalledWith(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe(mockExecutionId)
|
||||
})
|
||||
|
||||
it('should return null when conversion fails', () => {
|
||||
vi.spyOn(workflowStore, 'nodeLocatorIdToNodeExecutionId').mockReturnValue(
|
||||
null
|
||||
)
|
||||
|
||||
const result = store.nodeLocatorIdToExecutionId('invalid:format')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Subgraph } from '@comfyorg/litegraph'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
useWorkflowBookmarkStore,
|
||||
useWorkflowStore
|
||||
} from '@/stores/workflowStore'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
|
||||
// Add mock for api at the top of the file
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
@@ -26,10 +28,15 @@ vi.mock('@/scripts/api', () => ({
|
||||
// Mock comfyApp globally for the store setup
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: null // Start with canvas potentially undefined or null
|
||||
canvas: {} // Start with empty canvas object
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock isSubgraph
|
||||
vi.mock('@/utils/typeGuardUtil', () => ({
|
||||
isSubgraph: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
describe('useWorkflowStore', () => {
|
||||
let store: ReturnType<typeof useWorkflowStore>
|
||||
let bookmarkStore: ReturnType<typeof useWorkflowBookmarkStore>
|
||||
@@ -518,8 +525,13 @@ describe('useWorkflowStore', () => {
|
||||
{ name: 'Level 1 Subgraph' },
|
||||
{ name: 'Level 2 Subgraph' }
|
||||
]
|
||||
}
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
|
||||
} as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph
|
||||
|
||||
// Mock isSubgraph to return true for our mockSubgraph
|
||||
vi.mocked(isSubgraph).mockImplementation(
|
||||
(obj): obj is Subgraph => obj === mockSubgraph
|
||||
)
|
||||
|
||||
// Act: Trigger the update
|
||||
store.updateActiveGraph()
|
||||
@@ -536,8 +548,13 @@ describe('useWorkflowStore', () => {
|
||||
name: 'Initial Subgraph',
|
||||
pathToRootGraph: [{ name: 'Root' }, { name: 'Initial Subgraph' }],
|
||||
isRootGraph: false
|
||||
}
|
||||
vi.mocked(comfyApp.canvas).subgraph = initialSubgraph as any
|
||||
} as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = initialSubgraph
|
||||
|
||||
// Mock isSubgraph to return true for our initialSubgraph
|
||||
vi.mocked(isSubgraph).mockImplementation(
|
||||
(obj): obj is Subgraph => obj === initialSubgraph
|
||||
)
|
||||
|
||||
// Trigger initial update based on the *first* workflow opened in beforeEach
|
||||
store.updateActiveGraph()
|
||||
@@ -561,6 +578,11 @@ describe('useWorkflowStore', () => {
|
||||
// This ensures the watcher *does* cause a state change we can assert
|
||||
vi.mocked(comfyApp.canvas).subgraph = undefined
|
||||
|
||||
// Mock isSubgraph to return false for undefined
|
||||
vi.mocked(isSubgraph).mockImplementation(
|
||||
(_obj): _obj is Subgraph => false
|
||||
)
|
||||
|
||||
await store.openWorkflow(workflow2) // This changes activeWorkflow and triggers the watch
|
||||
await nextTick() // Allow watcher and potential async operations in updateActiveGraph to complete
|
||||
|
||||
@@ -569,4 +591,131 @@ describe('useWorkflowStore', () => {
|
||||
expect(store.activeSubgraph).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('NodeLocatorId conversions', () => {
|
||||
beforeEach(() => {
|
||||
// Setup mock graph structure with subgraphs
|
||||
const mockSubgraph = {
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
_nodes: []
|
||||
}
|
||||
|
||||
const mockNode = {
|
||||
id: 123,
|
||||
isSubgraphNode: () => true,
|
||||
subgraph: mockSubgraph
|
||||
}
|
||||
|
||||
const mockRootGraph = {
|
||||
_nodes: [mockNode],
|
||||
subgraphs: new Map([[mockSubgraph.id, mockSubgraph]]),
|
||||
getNodeById: (id: string | number) => {
|
||||
if (String(id) === '123') return mockNode
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
vi.mocked(comfyApp).graph = mockRootGraph as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
|
||||
store.activeSubgraph = mockSubgraph as any
|
||||
})
|
||||
|
||||
describe('nodeIdToNodeLocatorId', () => {
|
||||
it('should convert node ID to NodeLocatorId for subgraph nodes', () => {
|
||||
const result = store.nodeIdToNodeLocatorId(456)
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should return simple node ID for root graph nodes', () => {
|
||||
store.activeSubgraph = undefined
|
||||
const result = store.nodeIdToNodeLocatorId(123)
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should use provided subgraph instead of active one', () => {
|
||||
const customSubgraph = {
|
||||
id: 'custom-uuid-1234-5678-90ab-cdef12345678'
|
||||
} as any
|
||||
const result = store.nodeIdToNodeLocatorId(789, customSubgraph)
|
||||
expect(result).toBe('custom-uuid-1234-5678-90ab-cdef12345678:789')
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeExecutionIdToNodeLocatorId', () => {
|
||||
it('should convert execution ID to NodeLocatorId', () => {
|
||||
const result = store.nodeExecutionIdToNodeLocatorId('123:456')
|
||||
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
|
||||
})
|
||||
|
||||
it('should return simple node ID for root level nodes', () => {
|
||||
const result = store.nodeExecutionIdToNodeLocatorId('123')
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null for invalid execution IDs', () => {
|
||||
const result = store.nodeExecutionIdToNodeLocatorId('999:456')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToNodeId', () => {
|
||||
it('should extract node ID from NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe(456)
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const result = store.nodeLocatorIdToNodeId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:node_1'
|
||||
)
|
||||
expect(result).toBe('node_1')
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = store.nodeLocatorIdToNodeId('123')
|
||||
expect(result).toBe(123)
|
||||
|
||||
const stringResult = store.nodeLocatorIdToNodeId('node_1')
|
||||
expect(stringResult).toBe('node_1')
|
||||
})
|
||||
|
||||
it('should return null for invalid NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeId('invalid:format')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeLocatorIdToNodeExecutionId', () => {
|
||||
it('should convert NodeLocatorId to execution ID', () => {
|
||||
// Need to mock isSubgraph to identify our mockSubgraph
|
||||
vi.mocked(isSubgraph).mockImplementation((obj): obj is Subgraph => {
|
||||
return obj === store.activeSubgraph
|
||||
})
|
||||
|
||||
const result = store.nodeLocatorIdToNodeExecutionId(
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
|
||||
)
|
||||
expect(result).toBe('123:456')
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = store.nodeLocatorIdToNodeExecutionId('123')
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('should return null for unknown subgraph UUID', () => {
|
||||
const result = store.nodeLocatorIdToNodeExecutionId(
|
||||
'unknown-uuid-1234-5678-90ab-cdef12345678:456'
|
||||
)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for invalid NodeLocatorId', () => {
|
||||
const result = store.nodeLocatorIdToNodeExecutionId('invalid:format')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
207
tests-ui/tests/types/nodeIdentification.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import {
|
||||
type NodeLocatorId,
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
isNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
|
||||
describe('nodeIdentification', () => {
|
||||
describe('NodeLocatorId', () => {
|
||||
const validUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const validNodeId = '123'
|
||||
const validNodeLocatorId = `${validUuid}:${validNodeId}` as NodeLocatorId
|
||||
|
||||
describe('isNodeLocatorId', () => {
|
||||
it('should return true for valid NodeLocatorId', () => {
|
||||
expect(isNodeLocatorId(validNodeLocatorId)).toBe(true)
|
||||
expect(isNodeLocatorId(`${validUuid}:456`)).toBe(true)
|
||||
expect(isNodeLocatorId(`${validUuid}:node_1`)).toBe(true)
|
||||
// Simple node IDs (root graph)
|
||||
expect(isNodeLocatorId('123')).toBe(true)
|
||||
expect(isNodeLocatorId('node_1')).toBe(true)
|
||||
expect(isNodeLocatorId('5')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for invalid formats', () => {
|
||||
expect(isNodeLocatorId('123:456')).toBe(false) // No UUID in first part
|
||||
expect(isNodeLocatorId('not-a-uuid:123')).toBe(false)
|
||||
expect(isNodeLocatorId('')).toBe(false) // Empty string
|
||||
expect(isNodeLocatorId(':123')).toBe(false) // Empty UUID
|
||||
expect(isNodeLocatorId(`${validUuid}:`)).toBe(false) // Empty node ID
|
||||
expect(isNodeLocatorId(`${validUuid}:123:456`)).toBe(false) // Too many parts
|
||||
expect(isNodeLocatorId(123)).toBe(false) // Not a string
|
||||
expect(isNodeLocatorId(null)).toBe(false)
|
||||
expect(isNodeLocatorId(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('should validate UUID format correctly', () => {
|
||||
// Valid UUID formats
|
||||
expect(
|
||||
isNodeLocatorId('00000000-0000-0000-0000-000000000000:123')
|
||||
).toBe(true)
|
||||
expect(
|
||||
isNodeLocatorId('A1B2C3D4-E5F6-7890-ABCD-EF1234567890:123')
|
||||
).toBe(true)
|
||||
|
||||
// Invalid UUID formats
|
||||
expect(isNodeLocatorId('00000000-0000-0000-0000-00000000000:123')).toBe(
|
||||
false
|
||||
) // Too short
|
||||
expect(
|
||||
isNodeLocatorId('00000000-0000-0000-0000-0000000000000:123')
|
||||
).toBe(false) // Too long
|
||||
expect(
|
||||
isNodeLocatorId('00000000_0000_0000_0000_000000000000:123')
|
||||
).toBe(false) // Wrong separator
|
||||
expect(
|
||||
isNodeLocatorId('g0000000-0000-0000-0000-000000000000:123')
|
||||
).toBe(false) // Invalid hex
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseNodeLocatorId', () => {
|
||||
it('should parse valid NodeLocatorId', () => {
|
||||
const result = parseNodeLocatorId(validNodeLocatorId)
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: validUuid,
|
||||
localNodeId: 123
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const stringNodeId = `${validUuid}:node_1`
|
||||
const result = parseNodeLocatorId(stringNodeId)
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: validUuid,
|
||||
localNodeId: 'node_1'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle simple node IDs (root graph)', () => {
|
||||
const result = parseNodeLocatorId('123')
|
||||
expect(result).toEqual({
|
||||
subgraphUuid: null,
|
||||
localNodeId: 123
|
||||
})
|
||||
|
||||
const stringResult = parseNodeLocatorId('node_1')
|
||||
expect(stringResult).toEqual({
|
||||
subgraphUuid: null,
|
||||
localNodeId: 'node_1'
|
||||
})
|
||||
})
|
||||
|
||||
it('should return null for invalid formats', () => {
|
||||
expect(parseNodeLocatorId('123:456')).toBeNull() // No UUID in first part
|
||||
expect(parseNodeLocatorId('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createNodeLocatorId', () => {
|
||||
it('should create NodeLocatorId from components', () => {
|
||||
const result = createNodeLocatorId(validUuid, 123)
|
||||
expect(result).toBe(validNodeLocatorId)
|
||||
expect(isNodeLocatorId(result)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle string node IDs', () => {
|
||||
const result = createNodeLocatorId(validUuid, 'node_1')
|
||||
expect(result).toBe(`${validUuid}:node_1`)
|
||||
expect(isNodeLocatorId(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('NodeExecutionId', () => {
|
||||
describe('isNodeExecutionId', () => {
|
||||
it('should return true for execution IDs', () => {
|
||||
expect(isNodeExecutionId('123:456')).toBe(true)
|
||||
expect(isNodeExecutionId('123:456:789')).toBe(true)
|
||||
expect(isNodeExecutionId('node_1:node_2')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for non-execution IDs', () => {
|
||||
expect(isNodeExecutionId('123')).toBe(false)
|
||||
expect(isNodeExecutionId('node_1')).toBe(false)
|
||||
expect(isNodeExecutionId('')).toBe(false)
|
||||
expect(isNodeExecutionId(123)).toBe(false)
|
||||
expect(isNodeExecutionId(null)).toBe(false)
|
||||
expect(isNodeExecutionId(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseNodeExecutionId', () => {
|
||||
it('should parse execution IDs correctly', () => {
|
||||
expect(parseNodeExecutionId('123:456')).toEqual([123, 456])
|
||||
expect(parseNodeExecutionId('123:456:789')).toEqual([123, 456, 789])
|
||||
expect(parseNodeExecutionId('node_1:node_2')).toEqual([
|
||||
'node_1',
|
||||
'node_2'
|
||||
])
|
||||
expect(parseNodeExecutionId('123:node_2:456')).toEqual([
|
||||
123,
|
||||
'node_2',
|
||||
456
|
||||
])
|
||||
})
|
||||
|
||||
it('should return null for non-execution IDs', () => {
|
||||
expect(parseNodeExecutionId('123')).toBeNull()
|
||||
expect(parseNodeExecutionId('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createNodeExecutionId', () => {
|
||||
it('should create execution IDs from node arrays', () => {
|
||||
expect(createNodeExecutionId([123, 456])).toBe('123:456')
|
||||
expect(createNodeExecutionId([123, 456, 789])).toBe('123:456:789')
|
||||
expect(createNodeExecutionId(['node_1', 'node_2'])).toBe(
|
||||
'node_1:node_2'
|
||||
)
|
||||
expect(createNodeExecutionId([123, 'node_2', 456])).toBe(
|
||||
'123:node_2:456'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle single node ID', () => {
|
||||
const result = createNodeExecutionId([123])
|
||||
expect(result).toBe('123')
|
||||
// Single node IDs are not execution IDs
|
||||
expect(isNodeExecutionId(result)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
expect(createNodeExecutionId([])).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration tests', () => {
|
||||
it('should round-trip NodeLocatorId correctly', () => {
|
||||
const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const nodeId: NodeId = 123
|
||||
|
||||
const locatorId = createNodeLocatorId(uuid, nodeId)
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
|
||||
expect(parsed).toBeTruthy()
|
||||
expect(parsed!.subgraphUuid).toBe(uuid)
|
||||
expect(parsed!.localNodeId).toBe(nodeId)
|
||||
})
|
||||
|
||||
it('should round-trip NodeExecutionId correctly', () => {
|
||||
const nodeIds: NodeId[] = [123, 'node_2', 456]
|
||||
|
||||
const executionId = createNodeExecutionId(nodeIds)
|
||||
const parsed = parseNodeExecutionId(executionId)
|
||||
|
||||
expect(parsed).toEqual(nodeIds)
|
||||
})
|
||||
})
|
||||
})
|
||||