AllanHill commited on
Commit
26631b7
·
verified ·
1 Parent(s): 6272811

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +598 -786
app.py CHANGED
@@ -1,886 +1,698 @@
 
1
  import requests
2
  import re
3
  import json
4
  from datetime import datetime
5
  import os
6
- import gradio as gr
7
  from dotenv import load_dotenv
8
-
9
- # ========== LOAD ENVIRONMENT VARIABLES ==========
10
- def load_environment_variables():
11
- """Load environment variables for both local and Hugging Face"""
12
- # For Hugging Face Spaces (use environment variables)
13
- huggingface_url = os.environ.get('WORDPRESS_URL')
14
- huggingface_username = os.environ.get('WORDPRESS_USERNAME')
15
- huggingface_password = os.environ.get('WORDPRESS_APP_PASSWORD')
16
- huggingface_status = os.environ.get('DEFAULT_STATUS')
17
-
18
- # If Hugging Face env vars exist, use them
19
- if huggingface_url and huggingface_username:
20
- return {
21
- 'url': huggingface_url,
22
- 'username': huggingface_username,
23
- 'password': huggingface_password or '',
24
- 'status': huggingface_status or 'draft'
25
- }
26
-
27
- # Fallback to local .env file for development
28
- try:
29
- load_dotenv(dotenv_path='rank_cd.env')
30
- return {
31
- 'url': os.getenv('WORDPRESS_URL', 'https://cdgarment.com'),
32
- 'username': os.getenv('WORDPRESS_USERNAME', ''),
33
- 'password': os.getenv('WORDPRESS_APP_PASSWORD', ''),
34
- 'status': os.getenv('DEFAULT_STATUS', 'draft')
35
- }
36
- except Exception as e:
37
- print(f"Could not load rank_cd.env: {str(e)}")
38
- return {
39
- 'url': 'https://cdgarment.com',
40
- 'username': '',
41
- 'password': '',
42
- 'status': 'draft'
43
- }
44
-
45
- # ========== FUNCTIONS ==========
46
- def parse_gemini_content(content, post_status='draft'):
47
- """Parse Gemini content for cdgarment.com with Rank Math"""
48
- data = {
49
- 'seo_title': '',
50
- 'primary_keyword': '',
51
- 'meta_description': '',
52
- 'tags': [],
53
- 'article_title': '',
54
- 'content': '',
55
- 'url_slug': '',
56
- 'rank_math_meta': {}
57
- }
58
-
59
- try:
60
- # Clean content
61
- content = re.sub(r'\r\n', '\n', content)
62
- content = re.sub(r'\n\s*\n', '\n\n', content)
63
-
64
- # Extract SEO Title
65
- seo_title_match = re.search(r'SEO\s*Title\s*:?\s*(.+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL)
66
- if seo_title_match:
67
- data['seo_title'] = seo_title_match.group(1).strip()
68
-
69
- # Extract Primary Keyword
70
- keyword_match = re.search(r'Primary\s*Keyword\s*:?\s*(.+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL)
71
- if keyword_match:
72
- data['primary_keyword'] = keyword_match.group(1).strip()
73
-
74
- # Extract Meta Description
75
- meta_match = re.search(r'Meta\s*Description\s*:?\s*(.+?)(?=\n\s*\n|\nCharacter|\nTags|\n---|$)', content, re.IGNORECASE | re.DOTALL)
76
- if meta_match:
77
- data['meta_description'] = meta_match.group(1).strip()
78
-
79
- # Extract Tags
80
- tags_patterns = [
81
- r'Tags\s*:?\s*(.+?)(?=\n\s*\n|\n---|\n##|\n📝|$)', # Tags: on same line
82
- r'Tags\s*\n(.+?)(?=\n\s*\n|\n---|\n##|$)' # Tags on one line, actual tags on next
83
  ]
84
-
85
- tags_str = ""
86
- for pattern in tags_patterns:
87
- tags_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  if tags_match:
89
  tags_str = tags_match.group(1).strip()
90
- break
91
-
92
- if tags_str:
93
- # Clean and split tags
94
- tags_list = []
95
- for tag in re.split(r'[,;\n]', tags_str):
96
- cleaned_tag = tag.strip()
97
- if cleaned_tag and cleaned_tag.lower() not in ['', 'tags', 'tags:']:
98
- tags_list.append(cleaned_tag)
99
- data['tags'] = tags_list
100
-
101
- # Extract Article Content
102
- content_patterns = [
103
- r'---\s*\n(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)', # After ---
104
- r'📝\s*Article[^\n]*\n(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)', # After emoji
105
- r'##\s+(.+?)(?=\n##\s|\n###\s|\nConclusion:|$)' # After heading
106
- ]
107
-
108
- for pattern in content_patterns:
109
- article_match = re.search(pattern, content, re.DOTALL)
110
  if article_match:
111
  full_content = article_match.group(1).strip()
112
  lines = full_content.split('\n')
113
  if lines:
114
- # Remove markdown heading markers if present
115
- title = lines[0].strip()
116
- title = re.sub(r'^#+\s*', '', title)
117
- data['article_title'] = title
118
  data['content'] = '\n'.join(lines[1:])
119
- break
120
-
121
- # Generate URL slug
122
- if data['seo_title']:
123
- slug = data['seo_title'].lower()
124
- slug = re.sub(r'[^\w\s-]', '', slug)
125
- slug = re.sub(r'[-\s]+', '-', slug)
126
- data['url_slug'] = slug[:100]
127
-
128
- # Fallback title
129
- if not data['seo_title'] and data['article_title']:
130
- data['seo_title'] = data['article_title']
131
-
132
- # Prepare Rank Math meta fields
133
- data['rank_math_meta'] = {
134
- 'rank_math_title': data['seo_title'],
135
- 'rank_math_description': data['meta_description'],
136
- 'rank_math_robots': ['index'] if post_status == 'publish' else ['noindex'],
137
- 'rank_math_news_sitemap_robots': 'index',
138
- 'rank_math_facebook_title': data['seo_title'],
139
- 'rank_math_facebook_description': data['meta_description'],
140
- 'rank_math_twitter_title': data['seo_title'],
141
- 'rank_math_twitter_description': data['meta_description'],
142
- 'rank_math_canonical_url': '',
 
 
143
  }
144
-
145
- # Add primary keyword if available
146
- if data['primary_keyword']:
147
- data['rank_math_meta']['rank_math_focus_keyword'] = data['primary_keyword']
148
-
149
- return data
150
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  except Exception as e:
152
- print(f"Error parsing: {str(e)}")
153
- return {'error': f"Error parsing: {str(e)}"}
154
 
155
  def upload_image_to_wordpress(image_file, wp_config, filename_slug):
156
- """Upload image to WordPress with auto-naming"""
157
  try:
158
- # Handle file path from Gradio
159
- if isinstance(image_file, str):
160
- with open(image_file, 'rb') as f:
161
- image_data = f.read()
162
- file_extension = image_file.split('.')[-1].lower()
163
- else:
164
- # Handle file object
165
- image_data = image_file.read()
166
- file_extension = image_file.name.split('.')[-1].lower()
167
-
168
- # Generate filename from slug
169
- filename = f"{filename_slug}.{file_extension}"
170
-
171
- # Prepare image data
 
 
 
 
 
 
 
 
 
 
172
  files = {
173
- 'file': (filename, image_data, f'image/{file_extension}')
174
  }
175
-
176
  auth = (wp_config['username'], wp_config['password'])
177
-
178
- # Upload to WordPress
179
- response = requests.post(
180
- f"{wp_config['url']}/wp-json/wp/v2/media",
181
- auth=auth,
182
- files=files
183
- )
184
-
185
- if response.status_code == 201:
186
- media_data = response.json()
187
-
188
- # Update alt text with SEO title
189
- update_response = requests.post(
190
- f"{wp_config['url']}/wp-json/wp/v2/media/{media_data['id']}",
191
- auth=auth,
192
- json={
193
- 'alt_text': filename_slug.replace('-', ' ').title(),
194
- 'caption': filename_slug.replace('-', ' ').title(),
195
- 'description': f"Featured image for: {filename_slug.replace('-', ' ').title()}"
196
- }
197
- )
198
-
199
- return media_data['id']
200
- else:
201
- print(f"Image upload failed: {response.text}")
202
- return None
203
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  except Exception as e:
205
- print(f"Error uploading image: {str(e)}")
206
- return None
207
 
208
- def create_wordpress_post(parsed_data, wp_config, media_id=None, post_status='draft'):
209
- """Create WordPress post with Rank Math meta"""
210
  try:
211
- # Base post data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  post_data = {
213
- 'title': parsed_data['seo_title'],
214
- 'content': parsed_data['content'],
215
- 'slug': parsed_data['url_slug'],
216
  'status': post_status,
217
- 'meta': parsed_data['rank_math_meta']
218
  }
219
-
220
- # Add tags if available
221
- if parsed_data['tags']:
222
- post_data['tags'] = parsed_data['tags']
223
-
224
- # Add category (default to uncategorized)
 
 
 
 
 
225
  post_data['categories'] = [1]
226
-
227
- # Add featured image
228
  if media_id:
229
  post_data['featured_media'] = media_id
230
-
231
- # Try to set social images
232
  try:
233
- # Get media URL
234
  media_response = requests.get(
235
  f"{wp_config['url']}/wp-json/wp/v2/media/{media_id}",
236
  auth=(wp_config['username'], wp_config['password'])
237
  )
238
  if media_response.status_code == 200:
239
  media_url = media_response.json().get('source_url')
240
- post_data['meta']['rank_math_facebook_image'] = media_url
241
- post_data['meta']['rank_math_twitter_image'] = media_url
242
  except:
243
  pass
244
-
245
- # Send to WordPress
246
  response = requests.post(
247
  f"{wp_config['url']}/wp-json/wp/v2/posts",
248
  auth=(wp_config['username'], wp_config['password']),
249
  json=post_data,
250
  headers={'Content-Type': 'application/json'}
251
  )
252
-
253
  if response.status_code == 201:
254
- # Update canonical URL with actual post URL
255
  post_result = response.json()
256
  update_data = {
257
  'meta': {
258
  'rank_math_canonical_url': post_result['link']
259
  }
260
  }
261
-
262
  update_response = requests.post(
263
  f"{wp_config['url']}/wp-json/wp/v2/posts/{post_result['id']}",
264
  auth=(wp_config['username'], wp_config['password']),
265
  json=update_data
266
  )
267
-
268
- return post_result
269
  else:
270
- print(f"Post creation failed: {response.text}")
271
- return None
272
-
273
  except Exception as e:
274
- print(f"Error creating post: {str(e)}")
275
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
- # ========== GRADIO UI FUNCTIONS ==========
278
- def parse_content(gemini_content, post_status):
279
- """Parse Gemini content and return preview"""
280
- if not gemini_content or not gemini_content.strip():
281
- return "Please paste content first", "", "", "", "", "", "", {}
282
-
283
- parsed = parse_gemini_content(gemini_content, post_status)
284
-
285
- if 'error' in parsed:
286
- return parsed['error'], "", "", "", "", "", "", {}
287
-
288
- # Ensure all fields exist
289
- parsed.setdefault('seo_title', 'No title')
290
- parsed.setdefault('meta_description', '')
291
- parsed.setdefault('primary_keyword', '')
292
- parsed.setdefault('tags', [])
293
- parsed.setdefault('content', '')
294
- parsed.setdefault('url_slug', '')
295
-
296
- # Format tags for display
297
- if parsed['tags'] and len(parsed['tags']) > 0:
298
- tags_display = ", ".join(parsed['tags'][:8])
299
- else:
300
- tags_display = "No tags found in content"
301
-
302
- # Format meta description preview
303
- if parsed['meta_description']:
304
- meta_preview = parsed['meta_description'][:120] + "..." if len(parsed['meta_description']) > 120 else parsed['meta_description']
305
- else:
306
- meta_preview = "No meta description found"
307
-
308
- return (
309
- f"✅ Parsed: {parsed['seo_title'][:50]}..." if len(parsed['seo_title']) > 50 else f"✅ Parsed: {parsed['seo_title']}",
310
- parsed['seo_title'] or "No title extracted",
311
- parsed['url_slug'] or "No slug generated",
312
- meta_preview,
313
- parsed['primary_keyword'] or "No primary keyword",
314
- tags_display,
315
- f"Content Length: {len(parsed['content'])} characters",
316
- parsed
317
- )
318
 
319
- def publish_post(parsed_data, wp_config, image_file, post_status, wp_url, wp_username, wp_password):
320
- """Publish post to WordPress"""
321
- if not parsed_data:
322
- yield "❌ No parsed content available. Please parse content first.", None
323
- return
324
 
325
- # Use stored config or provided inputs
326
- if not wp_config:
327
- wp_config = {
328
- 'url': wp_url.rstrip('/') if wp_url else '',
329
- 'username': wp_username,
330
- 'password': wp_password,
331
- 'status': post_status
332
- }
333
 
334
- # Validate config
335
- if not all([wp_config['url'], wp_config['username'], wp_config['password']]):
336
- yield "❌ WordPress configuration incomplete. Please provide URL, username, and password.", None
337
- return
338
 
339
- try:
340
- yield "🖼️ Uploading image to WordPress...", None
341
-
342
- # Step 1: Upload image
343
- media_id = None
344
- if image_file:
345
- media_id = upload_image_to_wordpress(
346
- image_file,
347
- wp_config,
348
- parsed_data['url_slug']
349
- )
350
-
351
- yield "📤 Creating post with Rank Math meta...", None
352
-
353
- # Step 2: Create post
354
- result = create_wordpress_post(
355
- parsed_data,
356
  wp_config,
357
- media_id,
358
- post_status
359
  )
360
-
361
- if result:
362
- # Create download data
363
- export_data = {
364
- 'post_id': result['id'],
365
- 'title': result['title']['rendered'],
366
- 'link': result['link'],
367
- 'status': result['status'],
368
- 'slug': result['slug'],
369
- 'published_at': datetime.now().isoformat()
370
- }
371
-
372
- # Save JSON to file
373
- json_file = f"post_{result['id']}.json"
374
- with open(json_file, 'w') as f:
375
- json.dump(export_data, f, indent=2)
376
-
377
- success_msg = f"""
378
- Post published successfully!
379
-
380
- Post ID: {result['id']}
381
- Status: {result['status']}
382
- Date: {result['date'][:10]}
383
- Link: {result['link']}
384
-
385
- Click the download button to get post data.
386
- """
387
-
388
- yield success_msg, json_file
389
- else:
390
- yield "❌ Failed to publish post. Check WordPress configuration and try again.", None
391
-
392
- except Exception as e:
393
- yield f"❌ Error: {str(e)}", None
394
-
395
- def clear_all():
396
- """Clear all inputs"""
397
- return (
398
- "", # gemini_content
399
- None, # image
400
- "https://cdgarment.com", # wp_url
401
- "", # wp_username
402
- "", # wp_password
403
- "draft", # post_status
404
- "", # parse_status
405
- "", # seo_title
406
- "", # url_slug
407
- "", # meta_description
408
- "", # primary_keyword
409
- "", # tags
410
- "", # content_length
411
- "", # publish_status
412
- None, # download_file
413
- )
414
 
415
- # ========== CREATE GRADIO INTERFACE ==========
416
- with gr.Blocks(title="CdGarment WordPress Publisher") as demo:
417
-
418
- # State variables
419
- parsed_data_state = gr.State(None)
420
- wp_config_state = gr.State(None)
421
 
422
- # Header
423
- gr.HTML("""
424
- <div style="text-align: center; margin: 20px 0;">
425
- <h1 style="
426
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
427
- -webkit-background-clip: text;
428
- -webkit-text-fill-color: transparent;
429
- font-size: 2.5rem;
430
- font-weight: 800;
431
- margin-bottom: 10px;
432
- ">🏭 CdGarment WordPress Publisher</h1>
433
- <div style="
434
- background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
435
- color: #991B1B;
436
- padding: 8px 16px;
437
- border-radius: 20px;
438
- font-weight: 600;
439
- display: inline-block;
440
- margin-bottom: 20px;
441
- ">✓ Optimized for Rank Math SEO</div>
442
- </div>
443
- """)
444
-
445
- # Two main columns
446
  with gr.Row():
447
  with gr.Column(scale=2):
448
- with gr.Group():
449
- gr.Markdown("### 📋 Paste Gemini Content")
450
- gr.Markdown("Include SEO package and article. Formatting preserved!")
451
-
452
- # Formatting toolbar
453
- gr.HTML("""
454
- <div style="background: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px; border: 1px solid #e9ecef;">
455
- <strong>Formatting:</strong>
456
- <button class="format-btn" data-wrap="# " data-type="H1" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer;">H1</button>
457
- <button class="format-btn" data-wrap="## " data-type="H2" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer;">H2</button>
458
- <button class="format-btn" data-wrap="**" data-type="Bold" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer; font-weight: bold;">B</button>
459
- <button class="format-btn" data-wrap="*" data-type="Italic" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer; font-style: italic;">I</button>
460
- <button class="format-btn" data-wrap="- " data-type="List" style="background: white; border: 1px solid #dee2e6; padding: 5px 10px; margin-right: 5px; border-radius: 4px; cursor: pointer;">• List</button>
461
- </div>
462
-
463
- <script>
464
- function formatText(wrapText, type) {
465
- const textarea = document.querySelector('textarea[aria-label="Gemini Content"]');
466
- if (!textarea) return;
467
-
468
- const start = textarea.selectionStart;
469
- const end = textarea.selectionEnd;
470
- const selected = textarea.value.substring(start, end);
471
-
472
- if (selected) {
473
- textarea.value = textarea.value.substring(0, start) + wrapText + selected + wrapText + textarea.value.substring(end);
474
- textarea.selectionStart = start + wrapText.length;
475
- textarea.selectionEnd = end + wrapText.length;
476
- } else {
477
- textarea.value = textarea.value.substring(0, start) + wrapText + type + wrapText + textarea.value.substring(end);
478
- textarea.selectionStart = textarea.selectionEnd = start + wrapText.length + type.length;
479
- }
480
- textarea.focus();
481
-
482
- const event = new Event('input', { bubbles: true });
483
- textarea.dispatchEvent(event);
484
- }
485
-
486
- document.addEventListener('DOMContentLoaded', function() {
487
- const buttons = document.querySelectorAll('.format-btn');
488
- buttons.forEach(btn => {
489
- btn.addEventListener('click', function(e) {
490
- e.preventDefault();
491
- const wrap = this.getAttribute('data-wrap');
492
- const type = this.getAttribute('data-type');
493
- formatText(wrap, type);
494
- });
495
- });
496
- });
497
- </script>
498
- """)
499
-
500
- # Markdown editor
501
- example_content = """SEO Toolkit: Article #21
502
-
503
- Element
504
- Suggestion
505
-
506
- Primary Keyword
507
- Garment Supply Chain Security
508
-
509
- SEO Title
510
- Guaranteed Delivery: How CdGarment Solves the Global Sourcing Lead-Time Crisis
511
-
512
- Meta Description
513
- Eliminate delivery risks with CdGarment. Our advanced automated facilities and "In-Time" communication guarantee reliable lead times and supply chain security.
514
-
515
- Character Count
516
- 156 Characters
517
 
518
- Tags
519
- reliable garment supplier, apparel lead time management, secure supply chain fashion, automated apparel production, CdGarment delivery guarantee
520
 
521
  ---
522
 
523
- ## Guaranteed Delivery: Solving the Lead-Time Crisis Through Automation
524
-
525
- In 2026, the greatest risk to a fashion brand isn't design—it's **delivery**. Port delays, labor shortages, and manual production bottlenecks have made traditional "medium" manufacturers unreliable.
526
-
527
- CdGarment has solved this by rebuilding the manufacturing process around **Security and Stability**. When we commit to a date, our automated ecosystem ensures we meet it.
528
-
529
- ### 1. Automation = Predictability
530
-
531
- Manual factories suffer from "human variance." If a worker is sick or a bundle is lost, the whole line stops.
532
-
533
- - **The Hanging System Advantage**: Our automated system ensures a continuous flow.
534
- - **Buffer Management**: Our integrated bases allow us to shift production loads.
535
-
536
- ### 2. Real-Time Supply Chain Visibility
537
-
538
- Supply chain security comes from knowing, not guessing.
539
-
540
- - **Digital Tracking**: Every order is logged in our system.
541
- - **Proactive Logistics**: Our logistics team monitors global shipping lanes.
542
-
543
- ### 3. Financial and Compliance Security
544
-
545
- A secure supplier is a compliant supplier. CdGarment maintains all global certifications."""
546
-
547
- gemini_content = gr.Textbox(
548
- label="Gemini Content",
549
- value=example_content,
550
- lines=25,
551
- placeholder="""Paste your Gemini output here...
552
-
553
- Example format:
554
- SEO Title: Your Title Here
555
- Primary Keyword: Your Keyword
556
- Meta Description: Your description
557
- Tags: tag1, tag2, tag3
558
-
559
- --- (or 📝 Article)
560
-
561
- ## Article Title
562
- Your article content here...""",
563
- elem_classes="paste-box"
564
- )
565
-
566
- with gr.Row():
567
- parse_btn = gr.Button("🔍 Parse Content", variant="primary", scale=2)
568
- clear_btn = gr.Button("🗑️ Clear", variant="secondary", scale=1)
569
 
570
  with gr.Column(scale=1):
571
- # Image upload section
572
- with gr.Group():
573
- gr.Markdown("### 🖼️ Image & Upload")
574
- image_input = gr.File(
575
- label="Upload Image",
576
- file_types=["image"],
577
- type="filepath"
578
- )
579
- image_preview = gr.Image(
580
- label="Image Preview",
581
- height=200,
582
- visible=False
583
- )
584
- image_description = gr.Textbox(
585
- label="Image Description",
586
- placeholder="Auto-generated description will appear here",
587
- lines=3
588
- )
589
 
590
- # WordPress settings section
591
- with gr.Group():
592
- gr.Markdown("### ⚙️ WordPress Settings")
593
-
594
- # Load environment variables
595
- env_config = load_environment_variables()
596
-
597
- wp_url = gr.Textbox(
598
- label="WordPress URL",
599
- value=env_config['url'],
600
- placeholder="https://cdgarment.com"
601
- )
602
- wp_username = gr.Textbox(
603
- label="Username",
604
- value=env_config['username'],
605
- placeholder="admin"
606
- )
607
- wp_password = gr.Textbox(
608
- label="Application Password",
609
- value=env_config['password'],
610
- type="password",
611
- placeholder="••••••••"
612
- )
613
- post_status = gr.Radio(
614
- label="Post Status",
615
- choices=["draft", "publish"],
616
- value=env_config['status']
617
- )
618
-
619
- save_config_btn = gr.Button("💾 Save Configuration", variant="secondary")
620
-
621
- # Parse status and preview
622
- parse_status = gr.Textbox(label="Parse Status", visible=False)
623
-
624
- with gr.Row(visible=False) as preview_row:
625
- with gr.Column():
626
- seo_title = gr.Textbox(label="SEO Title")
627
- url_slug = gr.Textbox(label="URL Slug")
628
-
629
- with gr.Column():
630
- meta_description = gr.Textbox(label="Meta Description")
631
- primary_keyword = gr.Textbox(label="Primary Keyword")
632
-
633
- with gr.Column():
634
- tags = gr.Textbox(label="Tags")
635
- content_length = gr.Textbox(label="Content Length")
636
-
637
- # Publish section
638
- gr.Markdown("---")
639
- gr.Markdown("### 🚀 Ready to Publish")
640
-
641
- # Status indicators
642
- with gr.Row():
643
- content_status = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #fee2e2; color: #991b1b;'>❌ Content</span>")
644
- image_status = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #fef3c7; color: #92400e;'>⚠️ Image Optional</span>")
645
- wp_status = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #fee2e2; color: #991b1b;'>❌ WordPress</span>")
646
- status_indicator = gr.HTML("<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #e5e7eb;'>📊 Status: DRAFT</span>")
647
-
648
- publish_btn = gr.Button("🚀 PUSH TO WORDPRESS", variant="primary", size="lg")
649
-
650
- # Publish results
651
- publish_status = gr.Textbox(label="Publish Status", lines=5)
652
- download_file = gr.File(label="Download Post Data", visible=False)
653
-
654
- # Footer
655
- gr.Markdown("---")
656
- gr.Markdown(f"CdGarment WordPress Publisher • {datetime.now().year}")
657
-
658
- # ========== EVENT HANDLERS ==========
659
-
660
- def update_status_indicators(has_content, has_image, has_wp_config, post_status_value):
661
- """Update status indicators"""
662
- content_color = "#d1fae5" if has_content else "#fee2e2"
663
- content_text = "✅ Content" if has_content else "❌ Content"
664
- content_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: {content_color}; color: #065f46;'>{content_text}</span>"
665
-
666
- image_color = "#d1fae5" if has_image else "#fef3c7"
667
- image_text = "✅ Image" if has_image else "⚠️ Image Optional"
668
- image_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: {image_color}; color: #92400e;'>{image_text}</span>"
669
-
670
- wp_color = "#d1fae5" if has_wp_config else "#fee2e2"
671
- wp_text = "✅ WordPress" if has_wp_config else "❌ WordPress"
672
- wp_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: {wp_color}; color: #065f46;'>{wp_text}</span>"
673
-
674
- status_html = f"<span style='padding: 5px 10px; border-radius: 5px; font-weight: bold; margin: 2px; background-color: #e5e7eb;'>📊 Status: {post_status_value.upper()}</span>"
675
-
676
- return content_html, image_html, wp_html, status_html
677
-
678
- def check_wp_config(wp_url_val, wp_username_val, wp_password_val):
679
- """Check if WordPress config is complete"""
680
- return all([wp_url_val, wp_username_val, wp_password_val])
681
-
682
- # Image upload handler
683
- def update_image_preview(image_file):
684
- if image_file:
685
- return gr.update(visible=True, value=image_file), True
686
- return gr.update(visible=False), False
687
-
688
- # Parse button handler
689
- def on_parse_click(gemini_content_val, post_status_val):
690
- if not gemini_content_val or not gemini_content_val.strip():
691
- return (
692
- gr.update(value="Please paste content first", visible=True),
693
- gr.update(visible=False),
694
- "", "", "", "", "",
695
- None,
696
- *update_status_indicators(False, False, False, post_status_val)
697
  )
698
-
699
- result, parsed = parse_content(gemini_content_val, post_status_val)
700
-
701
- if isinstance(result, tuple):
702
- parse_status_val, seo_title_val, url_slug_val, meta_desc_val, keyword_val, tags_val, content_len_val = result[:7]
703
 
704
- # Debug output
705
- print(f"DEBUG - Parsed tags: {parsed.get('tags', [])}")
706
- print(f"DEBUG - Tags display: {tags_val}")
 
 
 
 
 
 
 
707
 
708
- return (
709
- gr.update(value=parse_status_val, visible=True),
710
- gr.update(visible=True),
711
- seo_title_val,
712
- url_slug_val,
713
- meta_desc_val,
714
- keyword_val,
715
- tags_val if tags_val else "No tags extracted",
716
- content_len_val,
717
- parsed,
718
- *update_status_indicators(True, False, False, post_status_val)
719
  )
720
- else:
721
- return (
722
- gr.update(value=result, visible=True),
723
- gr.update(visible=False),
724
- "", "", "", "", "",
725
- None,
726
- *update_status_indicators(False, False, False, post_status_val)
727
  )
728
-
729
- # Save config handler
730
- def on_save_config(wp_url_val, wp_username_val, wp_password_val, post_status_val):
731
- if all([wp_url_val, wp_username_val, wp_password_val]):
732
- wp_config = {
733
- 'url': wp_url_val.rstrip('/'),
734
- 'username': wp_username_val,
735
- 'password': wp_password_val,
736
- 'status': post_status_val
737
- }
738
- return (
739
- gr.update(value="✅ Configuration saved!"),
740
- wp_config,
741
- *update_status_indicators(False, False, True, post_status_val)
742
  )
743
- return (
744
- gr.update(value="⚠️ Please fill all WordPress configuration fields"),
745
- None,
746
- *update_status_indicators(False, False, False, post_status_val)
747
- )
748
-
749
- # Publish button handler
750
- def on_publish_click(parsed_data, wp_config, image_file, post_status_val, wp_url_val, wp_username_val, wp_password_val):
751
- if not parsed_data:
752
- yield (
753
- gr.update(value="❌ No parsed content available. Please parse content first"),
754
- gr.update(visible=False),
755
- *update_status_indicators(False, bool(image_file), bool(wp_config), post_status_val)
756
  )
757
- return
758
-
759
- wp_config_to_use = wp_config or {
760
- 'url': wp_url_val.rstrip('/') if wp_url_val else '',
761
- 'username': wp_username_val,
762
- 'password': wp_password_val,
763
- 'status': post_status_val
764
- }
765
-
766
- # Update status during processing
767
- yield (
768
- gr.update(value="🖼️ Uploading image to WordPress..."),
769
- gr.update(visible=False),
770
- *update_status_indicators(True, bool(image_file), True, post_status_val)
771
- )
772
-
773
- # Call the publish function
774
- for status_msg, download_file_val in publish_post(parsed_data, wp_config_to_use, image_file, post_status_val, wp_url_val, wp_username_val, wp_password_val):
775
- if download_file_val:
776
- yield (
777
- gr.update(value=status_msg),
778
- gr.update(value=download_file_val, visible=True),
779
- *update_status_indicators(True, bool(image_file), True, post_status_val)
780
- )
781
- else:
782
- yield (
783
- gr.update(value=status_msg),
784
- gr.update(visible=False),
785
- *update_status_indicators(True, bool(image_file), True, post_status_val)
786
- )
787
- break
788
-
789
- # Update status indicators when inputs change
790
- def update_indicators_on_change(gemini_content_val, image_file, wp_url_val, wp_username_val, wp_password_val, post_status_val, parsed_data):
791
- has_content = bool(parsed_data) or bool(gemini_content_val and gemini_content_val.strip())
792
- has_image = bool(image_file)
793
- has_wp = check_wp_config(wp_url_val, wp_username_val, wp_password_val)
794
- return update_status_indicators(has_content, has_image, has_wp, post_status_val)
795
 
796
- # ========== BIND EVENT HANDLERS ==========
 
797
 
798
- # Image upload
799
- image_input.change(
800
- update_image_preview,
801
- inputs=[image_input],
802
- outputs=[image_preview, image_status]
 
 
 
 
 
 
 
 
 
 
803
  )
804
 
805
- # Parse button
806
  parse_btn.click(
807
- on_parse_click,
808
- inputs=[gemini_content, post_status],
809
- outputs=[
810
- parse_status,
811
- preview_row,
812
- seo_title,
813
- url_slug,
814
- meta_description,
815
- primary_keyword,
816
- tags,
817
- content_length,
818
- parsed_data_state,
819
- content_status,
820
- image_status,
821
- wp_status,
822
- status_indicator
823
- ]
824
  )
825
 
826
- # Save config button
827
- save_config_btn.click(
828
- on_save_config,
829
- inputs=[wp_url, wp_username, wp_password, post_status],
830
- outputs=[parse_status, wp_config_state, content_status, image_status, wp_status, status_indicator]
831
  )
832
 
833
- # Publish button
834
- publish_btn.click(
835
- on_publish_click,
836
- inputs=[parsed_data_state, wp_config_state, image_input, post_status, wp_url, wp_username, wp_password],
837
- outputs=[publish_status, download_file, content_status, image_status, wp_status, status_indicator]
838
  )
839
 
840
- # Clear button
841
- clear_btn.click(
842
- clear_all,
843
- outputs=[
844
- gemini_content,
845
- image_input,
846
- wp_url,
847
- wp_username,
848
- wp_password,
849
- post_status,
850
- parse_status,
851
- seo_title,
852
- url_slug,
853
- meta_description,
854
- primary_keyword,
855
- tags,
856
- content_length,
857
- publish_status,
858
- download_file
859
- ]
860
  )
861
 
862
- # Update status indicators on input changes
863
- for input_component in [gemini_content, image_input, wp_url, wp_username, wp_password, post_status]:
864
- input_component.change(
865
- update_indicators_on_change,
866
- inputs=[gemini_content, image_input, wp_url, wp_username, wp_password, post_status, parsed_data_state],
867
- outputs=[content_status, image_status, wp_status, status_indicator]
868
- )
 
 
 
869
 
870
- # ========== LAUNCH THE APP ==========
871
  if __name__ == "__main__":
872
- demo.launch(
873
- server_name="0.0.0.0",
874
- server_port=7862,
875
- share=False,
876
- debug=True,
877
- ssr_mode=False,
878
- css="""
879
- .gradio-container { max-width: 1400px !important; }
880
- .paste-box textarea {
881
- font-family: 'Courier New', monospace !important;
882
- font-size: 14px !important;
883
- line-height: 1.5 !important;
884
- }
885
- """
886
- )
 
1
+ import gradio as gr
2
  import requests
3
  import re
4
  import json
5
  from datetime import datetime
6
  import os
 
7
  from dotenv import load_dotenv
8
+ import markdown
9
+ from PIL import Image
10
+ from io import BytesIO
11
+ import time
12
+
13
+ # ========== 加载环境变量 ==========
14
+ load_dotenv(dotenv_path='rank_cd.env')
15
+
16
+ # ========== 初始化全局状态 ==========
17
+ parsed_data = None
18
+ uploaded_image = None
19
+ image_description = ""
20
+ wp_config = {
21
+ 'url': os.getenv('WORDPRESS_URL', 'https://cdgarment.com'),
22
+ 'username': os.getenv('WORDPRESS_USERNAME', ''),
23
+ 'password': os.getenv('WORDPRESS_APP_PASSWORD', ''),
24
+ 'status': os.getenv('DEFAULT_STATUS', 'draft')
25
+ }
26
+ wp_config_locked = False
27
+ post_status = "draft"
28
+ parse_method = "auto"
29
+
30
+ # ========== 增强解析器类 ==========
31
+ class GeminiContentParser:
32
+ def __init__(self):
33
+ self.parsers = [
34
+ self.parse_json_format,
35
+ self.parse_markdown_table_format,
36
+ self.parse_simple_format
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  ]
38
+
39
+ def parse(self, content: str):
40
+ """尝试多种解析策略"""
41
+ for parser in self.parsers:
42
+ result = parser(content)
43
+ if result and (result.get('article_content') or result.get('content')):
44
+ return result
45
+
46
+ return None
47
+
48
+ def parse_json_format(self, content: str):
49
+ """解析JSON格式"""
50
+ json_patterns = [
51
+ r'```json\s*(.*?)\s*```',
52
+ r'{\s*"post_id".*?}',
53
+ r'.*# Machine-Readable Data.*?({.*})',
54
+ r'.*Machine-Readable Data.*?JSON.*?({.*})',
55
+ ]
56
+
57
+ for pattern in json_patterns:
58
+ match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)
59
+ if match:
60
+ try:
61
+ json_str = match.group(1)
62
+ except IndexError:
63
+ json_str = match.group(0)
64
+
65
+ try:
66
+ data = json.loads(json_str)
67
+
68
+ seo_toolkit = data.get('seo_toolkit', {})
69
+
70
+ article_content = data.get('article_content', '')
71
+ if not article_content:
72
+ before_json = content[:match.start()].strip()
73
+ after_json = content[match.end():].strip()
74
+ article_content = after_json if len(after_json) > len(before_json) else before_json
75
+
76
+ lines = article_content.split('\n')
77
+ article_title = lines[0].strip('# ').strip() if lines else ""
78
+
79
+ result = {
80
+ 'seo_title': seo_toolkit.get('seo_title', article_title),
81
+ 'primary_keyword': seo_toolkit.get('primary_keyword', ''),
82
+ 'secondary_keywords': seo_toolkit.get('secondary_keywords', []),
83
+ 'meta_description': seo_toolkit.get('meta_description', seo_toolkit.get('description', '')),
84
+ 'tags': seo_toolkit.get('tags', []),
85
+ 'article_title': article_title,
86
+ 'content': article_content,
87
+ 'post_id': data.get('post_id', ''),
88
+ 'character_count': seo_toolkit.get('character_count', 0),
89
+ 'parse_method': 'json'
90
+ }
91
+
92
+ if result['seo_title']:
93
+ slug = result['seo_title'].lower()
94
+ slug = re.sub(r'[^\w\s-]', '', slug)
95
+ slug = re.sub(r'[-\s]+', '-', slug)
96
+ result['url_slug'] = slug[:100]
97
+
98
+ return result
99
+ except json.JSONDecodeError:
100
+ continue
101
+
102
+ return None
103
+
104
+ def parse_markdown_table_format(self, content):
105
+ """解析Markdown表格格式"""
106
+ data = {
107
+ 'seo_title': '',
108
+ 'primary_keyword': '',
109
+ 'meta_description': '',
110
+ 'tags': [],
111
+ 'article_title': '',
112
+ 'content': '',
113
+ 'url_slug': '',
114
+ 'parse_method': 'markdown_table'
115
+ }
116
+
117
+ try:
118
+ seo_title_match = re.search(r'SEO Title:\s*(.+?)(?=\n)', content)
119
+ if seo_title_match:
120
+ data['seo_title'] = seo_title_match.group(1).strip()
121
+
122
+ keyword_match = re.search(r'Primary Keyword:\s*(.+?)(?=\n)', content)
123
+ if keyword_match:
124
+ data['primary_keyword'] = keyword_match.group(1).strip()
125
+
126
+ meta_match = re.search(r'Meta Description:\s*(.+?)(?=\n)', content)
127
+ if meta_match:
128
+ data['meta_description'] = meta_match.group(1).strip()
129
+
130
+ tags_match = re.search(r'Tags:\s*(.+?)(?=\n|📝|\$)', content)
131
  if tags_match:
132
  tags_str = tags_match.group(1).strip()
133
+ data['tags'] = [tag.strip() for tag in re.split(r'[,;]', tags_str) if tag.strip()]
134
+
135
+ article_match = re.search(r'(?:📝|▶|●|◆).*?(?:Article|Content)[:-]?\s*(.+)', content, re.DOTALL)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  if article_match:
137
  full_content = article_match.group(1).strip()
138
  lines = full_content.split('\n')
139
  if lines:
140
+ data['article_title'] = lines[0].strip()
 
 
 
141
  data['content'] = '\n'.join(lines[1:])
142
+
143
+ if data['seo_title']:
144
+ slug = data['seo_title'].lower()
145
+ slug = re.sub(r'[^\w\s-]', '', slug)
146
+ slug = re.sub(r'[-\s]+', '-', slug)
147
+ data['url_slug'] = slug[:100]
148
+
149
+ if not data['seo_title'] and data['article_title']:
150
+ data['seo_title'] = data['article_title']
151
+
152
+ return data
153
+
154
+ except Exception:
155
+ return None
156
+
157
+ def parse_simple_format(self, content):
158
+ """解析简单冒号分隔格式"""
159
+ data = {
160
+ 'seo_title': '',
161
+ 'primary_keyword': '',
162
+ 'meta_description': '',
163
+ 'tags': [],
164
+ 'article_title': '',
165
+ 'content': '',
166
+ 'url_slug': '',
167
+ 'parse_method': 'simple'
168
  }
169
+
170
+ try:
171
+ seo_title_match = re.search(r'SEO Title[:\s]+(.+)', content, re.IGNORECASE)
172
+ if not seo_title_match:
173
+ seo_title_match = re.search(r'Title[:\s]+(.+)', content, re.IGNORECASE)
174
+ if seo_title_match:
175
+ data['seo_title'] = seo_title_match.group(1).strip()
176
+
177
+ keyword_match = re.search(r'Primary Keyword[:\s]+(.+)', content, re.IGNORECASE)
178
+ if not keyword_match:
179
+ keyword_match = re.search(r'Keyword[:\s]+(.+)', content, re.IGNORECASE)
180
+ if keyword_match:
181
+ data['primary_keyword'] = keyword_match.group(1).strip()
182
+
183
+ meta_match = re.search(r'Meta Description[:\s]+(.+)', content, re.IGNORECASE)
184
+ if not meta_match:
185
+ meta_match = re.search(r'Description[:\s]+(.+)', content, re.IGNORECASE)
186
+ if meta_match:
187
+ data['meta_description'] = meta_match.group(1).strip()
188
+
189
+ tags_match = re.search(r'Tags[:\s]+(.+)', content, re.IGNORECASE)
190
+ if tags_match:
191
+ tags_str = tags_match.group(1).strip()
192
+ data['tags'] = [tag.strip() for tag in re.split(r'[,;]', tags_str) if tag.strip()]
193
+
194
+ article_match = re.search(r'(?:Article|Content)[:\s]+(.+)', content, re.DOTALL | re.IGNORECASE)
195
+ if not article_match:
196
+ metadata_end = content.find('\n\n')
197
+ if metadata_end != -1:
198
+ article_content = content[metadata_end:].strip()
199
+ lines = article_content.split('\n')
200
+ if lines:
201
+ data['article_title'] = lines[0].strip()
202
+ data['content'] = '\n'.join(lines[1:])
203
+ else:
204
+ full_content = article_match.group(1).strip()
205
+ lines = full_content.split('\n')
206
+ if lines:
207
+ data['article_title'] = lines[0].strip()
208
+ data['content'] = '\n'.join(lines[1:])
209
+
210
+ if data['seo_title']:
211
+ slug = data['seo_title'].lower()
212
+ slug = re.sub(r'[^\w\s-]', '', slug)
213
+ slug = re.sub(r'[-\s]+', '-', slug)
214
+ data['url_slug'] = slug[:100]
215
+
216
+ if not data['seo_title'] and data['article_title']:
217
+ data['seo_title'] = data['article_title']
218
+
219
+ return data
220
+
221
+ except Exception:
222
+ return None
223
+
224
+ # ========== 功能函数 ==========
225
+ def parse_gemini_content(content):
226
+ global parsed_data, parse_method
227
+ if not content or not content.strip():
228
+ return None, "❌ 内容为空"
229
+
230
+ parser = GeminiContentParser()
231
+ result = parser.parse(content)
232
+
233
+ if result:
234
+ parse_method = result.get('parse_method', 'unknown')
235
+ parsed_data = result
236
+
237
+ method_msg = {
238
+ 'json': '✅ 使用 JSON 格式解析',
239
+ 'markdown_table': 'ℹ️ 使用 Markdown 表格格式解析',
240
+ 'simple': 'ℹ️ 使用简单格式解析'
241
+ }.get(parse_method, f'ℹ️ 使用 {parse_method} 格式解析')
242
+
243
+ return result, method_msg
244
+
245
+ return None, "❌ 无法用任何已知格式解析内容"
246
+
247
+ def get_or_create_tag(tag_name, wp_config):
248
+ try:
249
+ auth = (wp_config['username'], wp_config['password'])
250
+
251
+ response = requests.get(
252
+ f"{wp_config['url']}/wp-json/wp/v2/tags",
253
+ auth=auth,
254
+ params={'search': tag_name, 'per_page': 10}
255
+ )
256
+
257
+ if response.status_code == 200:
258
+ tags = response.json()
259
+ for tag in tags:
260
+ if tag['name'].lower() == tag_name.lower():
261
+ return tag['id']
262
+
263
+ create_response = requests.post(
264
+ f"{wp_config['url']}/wp-json/wp/v2/tags",
265
+ auth=auth,
266
+ json={'name': tag_name}
267
+ )
268
+
269
+ if create_response.status_code == 201:
270
+ return create_response.json()['id']
271
+
272
+ return None
273
+
274
  except Exception as e:
275
+ return f" 处理标签 '{tag_name}' 时出错: {str(e)}"
 
276
 
277
  def upload_image_to_wordpress(image_file, wp_config, filename_slug):
 
278
  try:
279
+ img = Image.open(image_file)
280
+
281
+ if img.mode == 'RGBA':
282
+ background = Image.new('RGB', img.size, (255, 255, 255))
283
+ if 'A' in img.getbands():
284
+ background.paste(img, mask=img.split()[-1])
285
+ else:
286
+ background.paste(img)
287
+ img = background
288
+ elif img.mode != 'RGB':
289
+ img = img.convert('RGB')
290
+
291
+ max_size = 2000
292
+ if max(img.size) > max_size:
293
+ ratio = max_size / max(img.size)
294
+ new_size = tuple([int(dim * ratio) for dim in img.size])
295
+ img = img.resize(new_size, Image.Resampling.LANCZOS)
296
+
297
+ buffer = BytesIO()
298
+ img.save(buffer, format='JPEG', quality=90, optimize=True)
299
+ image_data = buffer.getvalue()
300
+
301
+ filename = f"{filename_slug}.jpg"
302
+
303
  files = {
304
+ 'file': (filename, image_data, 'image/jpeg')
305
  }
306
+
307
  auth = (wp_config['username'], wp_config['password'])
308
+
309
+ max_retries = 3
310
+ retry_delay = 2
311
+
312
+ for attempt in range(max_retries):
313
+ try:
314
+ response = requests.post(
315
+ f"{wp_config['url']}/wp-json/wp/v2/media",
316
+ auth=auth,
317
+ files=files,
318
+ timeout=60,
319
+ verify=False
320
+ )
321
+
322
+ if response.status_code == 201:
323
+ media_data = response.json()
324
+ try:
325
+ update_response = requests.post(
326
+ f"{wp_config['url']}/wp-json/wp/v2/media/{media_data['id']}",
327
+ auth=auth,
328
+ json={
329
+ 'alt_text': image_description,
330
+ 'caption': filename_slug.replace('-', ' ').title(),
331
+ 'description': f"Featured image for: {filename_slug.replace('-', ' ').title()}"
332
+ },
333
+ timeout=10
334
+ )
335
+ except Exception:
336
+ pass
337
+
338
+ return media_data['id'], f"✅ 图片上传成功!(ID: {media_data['id']})"
339
+
340
+ elif response.status_code == 413:
341
+ return None, "❌ 图片太大,请压缩后重试"
342
+
343
+ elif response.status_code == 401:
344
+ return None, "❌ 认证失败,请检查 WordPress 凭据"
345
+
346
+ else:
347
+ if attempt < max_retries - 1:
348
+ time.sleep(retry_delay * (attempt + 1))
349
+ continue
350
+ else:
351
+ return None, f"❌ 图片上传失败,状态码: {response.status_code}"
352
+
353
+ except requests.exceptions.Timeout:
354
+ if attempt < max_retries - 1:
355
+ time.sleep(retry_delay * (attempt + 1))
356
+ continue
357
+ else:
358
+ return None, "❌ 图片上传超时"
359
+
360
+ except requests.exceptions.ConnectionError:
361
+ if attempt < max_retries - 1:
362
+ time.sleep(retry_delay * (attempt + 1))
363
+ continue
364
+ else:
365
+ return None, "❌ 连接失败,请检查网络和服务器"
366
+
367
+ except Exception as e:
368
+ return None, f"❌ 上传时出错: {str(e)}"
369
+
370
+ return None, "❌ 图片上传失败"
371
+
372
  except Exception as e:
373
+ return None, f" 处理图片时出错: {str(e)}"
 
374
 
375
+ def create_wordpress_post(parsed_data, wp_config, media_id=None):
 
376
  try:
377
+ rank_math_meta = {
378
+ 'rank_math_title': parsed_data.get('seo_title', ''),
379
+ 'rank_math_description': parsed_data.get('meta_description', ''),
380
+ 'rank_math_robots': ['index'] if post_status == 'publish' else ['noindex'],
381
+ 'rank_math_news_sitemap_robots': 'index',
382
+ 'rank_math_facebook_title': parsed_data.get('seo_title', ''),
383
+ 'rank_math_facebook_description': parsed_data.get('meta_description', ''),
384
+ 'rank_math_twitter_title': parsed_data.get('seo_title', ''),
385
+ 'rank_math_twitter_description': parsed_data.get('meta_description', ''),
386
+ 'rank_math_canonical_url': '',
387
+ }
388
+
389
+ if parsed_data.get('primary_keyword'):
390
+ rank_math_meta['rank_math_focus_keyword'] = parsed_data['primary_keyword']
391
+
392
+ html_content = markdown.markdown(parsed_data.get('content', ''))
393
+
394
  post_data = {
395
+ 'title': parsed_data.get('seo_title', 'Untitled'),
396
+ 'content': html_content,
397
+ 'slug': parsed_data.get('url_slug', ''),
398
  'status': post_status,
399
+ 'meta': rank_math_meta
400
  }
401
+
402
+ if parsed_data.get('tags'):
403
+ tag_ids = []
404
+ for tag_name in parsed_data['tags']:
405
+ tag_id = get_or_create_tag(tag_name, wp_config)
406
+ if tag_id and isinstance(tag_id, int):
407
+ tag_ids.append(tag_id)
408
+
409
+ if tag_ids:
410
+ post_data['tags'] = tag_ids
411
+
412
  post_data['categories'] = [1]
413
+
 
414
  if media_id:
415
  post_data['featured_media'] = media_id
416
+
 
417
  try:
 
418
  media_response = requests.get(
419
  f"{wp_config['url']}/wp-json/wp/v2/media/{media_id}",
420
  auth=(wp_config['username'], wp_config['password'])
421
  )
422
  if media_response.status_code == 200:
423
  media_url = media_response.json().get('source_url')
424
+ rank_math_meta['rank_math_facebook_image'] = media_url
425
+ rank_math_meta['rank_math_twitter_image'] = media_url
426
  except:
427
  pass
428
+
 
429
  response = requests.post(
430
  f"{wp_config['url']}/wp-json/wp/v2/posts",
431
  auth=(wp_config['username'], wp_config['password']),
432
  json=post_data,
433
  headers={'Content-Type': 'application/json'}
434
  )
435
+
436
  if response.status_code == 201:
 
437
  post_result = response.json()
438
  update_data = {
439
  'meta': {
440
  'rank_math_canonical_url': post_result['link']
441
  }
442
  }
443
+
444
  update_response = requests.post(
445
  f"{wp_config['url']}/wp-json/wp/v2/posts/{post_result['id']}",
446
  auth=(wp_config['username'], wp_config['password']),
447
  json=update_data
448
  )
449
+
450
+ return post_result, None
451
  else:
452
+ return None, f"WordPress API 错误: {response.text}"
453
+
 
454
  except Exception as e:
455
+ return None, f"创建文章时出错: {str(e)}"
456
+
457
+ # ========== Gradio UI 回调函数 ==========
458
+ def parse_content_callback(content):
459
+ global parsed_data
460
+ result, msg = parse_gemini_content(content)
461
+ if result:
462
+ preview = f"""
463
+ ### 📊 解析结果
464
+ **标题:** {result['seo_title']}
465
+ **主关键词:** {result.get('primary_keyword', '无')}
466
+ **标签:** {', '.join(result.get('tags', []))}
467
+ **URL Slug:** {result.get('url_slug', '自动生成')}
468
+ **内容长度:** {len(result.get('content', ''))} 字符
469
+ """
470
+ return msg, preview
471
+ return msg, ""
472
+
473
+ def update_image_description_callback(image):
474
+ global uploaded_image, image_description
475
+ if image:
476
+ uploaded_image = image
477
+ filename = image.name
478
+ name_without_ext = '.'.join(filename.split('.')[:-1])
479
+ readable_name = name_without_ext.replace('_', ' ').replace('-', ' ').title()
480
+ image_description = f"Featured image: {readable_name}"
481
+ return image_description, gr.update(value=image_description)
482
+ return "", gr.update()
483
+
484
+ def save_config_callback(url, username, password, status):
485
+ global wp_config, wp_config_locked, post_status
486
+ if not wp_config_locked:
487
+ wp_config = {
488
+ 'url': url.rstrip('/'),
489
+ 'username': username,
490
+ 'password': password,
491
+ 'status': status
492
+ }
493
+ post_status = status
494
+ return "✅ 配置已保存!"
495
+ return "🔒 配置已锁定,无法保存"
496
 
497
+ def toggle_lock_callback(locked):
498
+ global wp_config_locked
499
+ wp_config_locked = locked
500
+ return gr.update(interactive=not locked), gr.update(interactive=not locked), gr.update(interactive=not locked)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
+ def publish_callback():
503
+ global parsed_data, uploaded_image, wp_config, post_status
 
 
 
504
 
505
+ if not parsed_data:
506
+ return "❌ 请先解析内容", "", ""
 
 
 
 
 
 
507
 
508
+ if not all([wp_config.get('url'), wp_config.get('username'), wp_config.get('password')]):
509
+ return "❌ WordPress 配置不完整", "", ""
 
 
510
 
511
+ yield "🖼️ 正在上传图片到 WordPress...", "", ""
512
+ media_id = None
513
+ if uploaded_image:
514
+ media_id, img_msg = upload_image_to_wordpress(
515
+ uploaded_image,
 
 
 
 
 
 
 
 
 
 
 
 
516
  wp_config,
517
+ parsed_data.get('url_slug', 'post')
 
518
  )
519
+ if not media_id:
520
+ return f"❌ {img_msg}", "", ""
521
+
522
+ yield "📤 正在创建文章并设置 Rank Math 元数据...", "", ""
523
+ result, error = create_wordpress_post(parsed_data, wp_config, media_id)
524
+
525
+ if result:
526
+ yield f"✅ 文章{'已发布' if post_status == 'publish' else '已保存为草稿'}!(ID: {result['id']})", f"""
527
+ ### 📝 发布成功
528
+ **文章ID:** {result['id']}
529
+ **状态:** {result['status']}
530
+ **日期:** {result['date'][:10]}
531
+ **链接:** {result['link']}
532
+ """, json.dumps({
533
+ 'post_id': result['id'],
534
+ 'title': result['title']['rendered'],
535
+ 'link': result['link'],
536
+ 'status': result['status'],
537
+ 'slug': result['slug'],
538
+ 'published_at': datetime.now().isoformat(),
539
+ 'parse_method': parse_method
540
+ }, indent=2)
541
+ else:
542
+ yield f"❌ 发布失败: {error}", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
544
+ # ========== Gradio UI 布局 ==========
545
+ with gr.Blocks(title="CdGarment WordPress Publisher", theme=gr.themes.Soft()) as demo:
546
+ gr.Markdown("# 🏭 CdGarment WordPress Publisher")
547
+ gr.Markdown("### 专为 Rank Math SEO 优化")
 
 
548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  with gr.Row():
550
  with gr.Column(scale=2):
551
+ gr.Markdown("### 📋 粘贴 Gemini 内容")
552
+ gr.Markdown("支持 JSON 和传统格式")
553
+
554
+ example_content = """# Article #25: Smart Manufacturing: AI & Automation in our Humen Factory
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
556
+ The apparel industry is undergoing a digital revolution...
 
557
 
558
  ---
559
 
560
+ # Machine-Readable Data (For your Script)
561
+
562
+ ```json
563
+ {
564
+ "post_id": "25",
565
+ "seo_toolkit": {
566
+ "primary_keyword": "AI in Garment Manufacturing",
567
+ "secondary_keywords": [
568
+ "Smart apparel factory Humen",
569
+ "Automated fabric cutting",
570
+ "Digital clothing production 2026"
571
+ ],
572
+ "seo_title": "Smart Manufacturing: AI & Automation in our Humen Factory | CdGarment",
573
+ "description": "Explore the future of fashion...",
574
+ "character_count": 159,
575
+ "tags": ["AI garment manufacturing", "smart factory", "automated apparel production", "digital fashion", "Humen factory"]
576
+ }
577
+ }
578
+ ```"""
579
+
580
+ content_input = gr.Textbox(
581
+ label="粘贴你的 Gemini 输出",
582
+ value=example_content,
583
+ lines=15,
584
+ placeholder="在此粘贴完整的 Gemini 输出(JSON 或传统格式)"
585
+ )
586
+
587
+ parse_btn = gr.Button("🔍 解析内容", variant="primary")
588
+ parse_msg = gr.Markdown()
589
+
590
+ preview_box = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
  with gr.Column(scale=1):
593
+ gr.Markdown("### 🖼️ 图片和上传")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
 
595
+ image_upload = gr.File(
596
+ label="上传特色图片",
597
+ file_types=["image"],
598
+ type="filepath"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  )
 
 
 
 
 
600
 
601
+ img_desc_input = gr.Textbox(
602
+ label="图片描述",
603
+ placeholder="输入图片的 alt 文本和描述",
604
+ value=""
605
+ )
606
+
607
+ image_preview = gr.Image(label="图片预览", height=200)
608
+
609
+ gr.Markdown("---")
610
+ gr.Markdown("### ⚙️ WordPress 设置")
611
 
612
+ lock_toggle = gr.Checkbox(label="🔒 锁定配置", value=False)
613
+
614
+ wp_url = gr.Textbox(
615
+ label="WordPress URL",
616
+ value=wp_config.get('url', 'https://cdgarment.com')
 
 
 
 
 
 
617
  )
618
+
619
+ wp_username = gr.Textbox(
620
+ label="用户名",
621
+ value=wp_config.get('username', ''),
622
+ placeholder="admin"
 
 
623
  )
624
+
625
+ wp_password = gr.Textbox(
626
+ label="应用密码",
627
+ value=wp_config.get('password', ''),
628
+ type="password",
629
+ placeholder="••••••••"
 
 
 
 
 
 
 
 
630
  )
631
+
632
+ status_select = gr.Radio(
633
+ choices=["draft", "publish"],
634
+ value=post_status,
635
+ label="文章状态"
 
 
 
 
 
 
 
 
636
  )
637
+
638
+ save_btn = gr.Button("💾 保存配置")
639
+ save_msg = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
 
641
+ gr.Markdown("---")
642
+ gr.Markdown("### 🚀 准备发布")
643
 
644
+ with gr.Row():
645
+ content_status = gr.Markdown("❌ 内容")
646
+ image_status = gr.Markdown("⚠️ 图片可选")
647
+ wp_status = gr.Markdown("❓ WordPress")
648
+ status_display = gr.Markdown(f"📊 状态: {post_status.upper()}")
649
+
650
+ publish_btn = gr.Button("🚀 推送到 WordPress", variant="primary", size="lg")
651
+ publish_progress = gr.Markdown()
652
+ publish_result = gr.Markdown()
653
+
654
+ json_output = gr.JSON(label="文章数据", visible=False)
655
+ download_btn = gr.DownloadButton(
656
+ "📥 下载文章数据",
657
+ value="",
658
+ visible=False
659
  )
660
 
661
+ # ========== 事件绑定 ==========
662
  parse_btn.click(
663
+ parse_content_callback,
664
+ inputs=[content_input],
665
+ outputs=[parse_msg, preview_box]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  )
667
 
668
+ image_upload.change(
669
+ update_image_description_callback,
670
+ inputs=[image_upload],
671
+ outputs=[img_desc_input, img_desc_input]
 
672
  )
673
 
674
+ lock_toggle.change(
675
+ toggle_lock_callback,
676
+ inputs=[lock_toggle],
677
+ outputs=[wp_url, wp_username, wp_password]
 
678
  )
679
 
680
+ save_btn.click(
681
+ save_config_callback,
682
+ inputs=[wp_url, wp_username, wp_password, status_select],
683
+ outputs=[save_msg]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
  )
685
 
686
+ publish_btn.click(
687
+ publish_callback,
688
+ inputs=[],
689
+ outputs=[publish_progress, publish_result, json_output]
690
+ ).then(
691
+ lambda: (gr.update(visible=True), gr.update(visible=True)),
692
+ outputs=[json_output, download_btn]
693
+ )
694
+
695
+ gr.Markdown(f"---\nCdGarment WordPress Publisher • {datetime.now().year} • 格式: {parse_method.upper()}")
696
 
 
697
  if __name__ == "__main__":
698
+ demo.launch(server_name="0.0.0.0", server_port=7860)