Tự động tạo hình ảnh đầu bài cho WordPress khi đăng bài bằng API
Một plugin nhỏ chạy ngầm sau khi bấm Đăng: đọc trường mô tả cảnh, gọi API AI Hình Ảnh, lưu webp vào thư viện, gắn làm hình đại diện và thẻ alt, tự thử lại khi máy bận.

Một trang tin ẩm thực đăng ba bài mỗi ngày, và việc tốn thời gian nhất không phải viết bài mà là đi tìm hình đầu bài: hình miễn phí thì trùng với cả trăm trang khác, hình tự chụp thì không phải món nào cũng có. Bài này dựng một plugin WordPress nhỏ: người viết gõ một câu mô tả cảnh vào trường tuỳ biến, bấm Đăng, vài chục giây sau bài tự có hình đại diện vẽ riêng, đúng khổ, có thẻ alt. Không cần cài thư viện nào, chỉ cần một khoá API.
Thiết kế: vì sao không vẽ ngay lúc bấm Đăng
Cách ngây thơ là móc vào sự kiện lưu bài và gọi API ngay. Đừng làm vậy. Mỗi lượt vẽ là đồng bộ và có thể mất tới khoảng 90 giây khi máy bận. Người viết bấm Đăng rồi nhìn vòng xoay một phút rưỡi, hoặc tệ hơn, máy chủ thuê cắt ngang yêu cầu ở giây thứ 30 hay 60 và bài đăng dở dang.
Thiết kế đúng tách hai bước:
| Bước | Khi nào | Làm gì |
|---|---|---|
| 1. Hẹn việc | Ngay khi bài chuyển sang trạng thái đã đăng | Kiểm tra điều kiện, hẹn một việc chạy ngầm sau 5 giây |
| 2. Vẽ và gắn | Trong WP-Cron, ngoài phiên của người viết | Gọi API, lưu tệp, tạo tệp đính kèm, gắn hình đại diện |
| 3. Thử lại | Khi gặp 503 hoặc lỗi mạng | Hẹn lại theo Retry-After, tối đa 5 lần |
Người viết bấm Đăng xong là xong. Hình đại diện xuất hiện sau, thường trong vòng một phút với hạng Plus.
Vì sao cần một trường mô tả cảnh riêng
Có thể bạn nghĩ: lấy luôn tiêu đề bài làm câu lệnh vẽ. Có hai lý do không nên:
- API không tự dịch. Trang web của AI Hình Ảnh có bước chuyển mô tả tiếng Việt thành câu lệnh vẽ tiếng Anh, còn API thì đưa thẳng nội dung
prompttới máy vẽ. Tiêu đề "10 quán bún chả ngon ở Hà Nội" gửi nguyên văn sẽ cho kết quả kém sát. - Tiêu đề không phải cảnh. "Cách bảo quản rau thơm được lâu" không nói máy vẽ cần vẽ gì, góc nào, ánh sáng ra sao.
Vì vậy plugin đọc một trường tuỳ biến tên ahn_canh: một câu tiếng Anh 15–30 từ tả cảnh. Bài nào không có trường này thì plugin bỏ qua, không vẽ bừa. Ví dụ cho bài về rau thơm:
Fresh Vietnamese herbs wrapped in damp paper towel inside a glass container on a kitchen counter, soft morning light, close-up, shallow depth of fieldNgười viết không quen tiếng Anh có thể gõ mô tả tiếng Việt trên ô vẽ một lần, xem dòng "Câu lệnh vẽ" dưới kết quả, rồi chép vào trường. Làm vài lần là quen khuôn câu.
Mã plugin
Đặt khoá trong wp-config.php, phía trên dòng "That's all, stop editing":
```php
define('AHN_KEY', 'ahn_XXXX');
```
Tạo tệp wp-content/mu-plugins/ahn-hinh-dau-bai.php (thư mục mu-plugins tự nạp, không cần bật trong trang quản trị; chưa có thì tạo):
```php
<?php
/**
- Plugin Name: AI Hình Ảnh - hình đầu bài tự động
- Description: Bài có trường ahn_canh mà chưa có hình đại diện thì vẽ bằng API sau khi đăng.
*/
if (!defined('ABSPATH')) exit;
const AHN_URL = 'https://aihinhanh.com/v1/images/generations';
add_action('transition_post_status', function ($moi, $cu, $post) {
if ($moi !== 'publish' || $cu === 'publish' || $post->post_type !== 'post') return;
if (has_post_thumbnail($post) || !get_post_meta($post->ID, 'ahn_canh', true)) return;
if (!wp_next_scheduled('ahn_ve_dau_bai', [$post->ID])) {
wp_schedule_single_event(time() + 5, 'ahn_ve_dau_bai', [$post->ID]);
}
}, 10, 3);
add_action('ahn_ve_dau_bai', 'ahn_ve_dau_bai');
function ahn_ve_dau_bai($post_id) {
$post = get_post($post_id);
if (!$post || has_post_thumbnail($post_id)) return;
$canh = trim((string) get_post_meta($post_id, 'ahn_canh', true));
if ($canh === '' || !defined('AHN_KEY')) return;
@set_time_limit(180);
$res = wp_remote_post(AHN_URL, [
'timeout' => 120,
'headers' => [
'Authorization' => 'Bearer ' . AHN_KEY,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'model' => get_post_meta($post_id, 'ahn_hang', true) ?: 'plus',
'prompt' => $canh . ', realistic editorial photo, no text, no logo',
'size' => '1536x1024',
'n' => 1,
'output_format' => 'webp',
]),
]);
if (is_wp_error($res)) { ahn_hen_lai($post_id, $res->get_error_message(), 120); return; }
$code = (int) wp_remote_retrieve_response_code($res);
$json = json_decode(wp_remote_retrieve_body($res), true);
if ($code === 503 || $code >= 500) {
$sau = (int) wp_remote_retrieve_header($res, 'retry-after') ?: 60;
ahn_hen_lai($post_id, "$code: " . ($json['error']['message'] ?? ''), $sau);
return;
}
if ($code !== 200 || empty($json['data'][0]['b64_json'])) {
update_post_meta($post_id, 'ahn_loi', "$code: " . ($json['error']['message'] ?? 'không rõ'));
return; // 400, 401, 402: không thử lại
}
$ten = sanitize_title($post->post_name ?: $post->post_title) . '-' . gmdate('ymd-His') . '.webp';
$up = wp_upload_bits($ten, null, base64_decode($json['data'][0]['b64_json']));
if (!empty($up['error'])) { update_post_meta($post_id, 'ahn_loi', $up['error']); return; }
require_once ABSPATH . 'wp-admin/includes/image.php';
$att = wp_insert_attachment([
'post_mime_type' => 'image/webp',
'post_title' => $post->post_title,
'post_status' => 'inherit',
], $up['file'], $post_id);
wp_update_attachment_metadata($att, wp_generate_attachment_metadata($att, $up['file']));
$alt = trim((string) get_post_meta($post_id, 'ahn_alt', true)) ?: $post->post_title;
update_post_meta($att, '_wp_attachment_image_alt', $alt);
update_post_meta($att, 'ahn_cau_lenh', $canh);
set_post_thumbnail($post_id, $att);
delete_post_meta($post_id, 'ahn_loi');
}
function ahn_hen_lai($post_id, $loi, $sau) {
$lan = (int) get_post_meta($post_id, 'ahn_lan', true) + 1;
update_post_meta($post_id, 'ahn_lan', $lan);
update_post_meta($post_id, 'ahn_loi', "lần $lan: $loi");
if ($lan <= 5) {
wp_schedule_single_event(time() + $sau + 30 * $lan, 'ahn_ve_dau_bai', [$post_id]);
}
}
```
Giải thích những chỗ quan trọng
transition_post_status thay vì save_post. save_post chạy mỗi lần lưu nháp, mỗi lần sửa chính tả. Chỉ cần vẽ đúng một lần khi bài từ trạng thái khác chuyển sang "đã đăng". Bài hẹn giờ cũng đi qua sự kiện này khi tới giờ đăng.
has_post_thumbnail kiểm hai lần. Một lần lúc hẹn, một lần lúc chạy. Nếu trong 5 giây đó biên tập viên kịp chọn hình thủ công, plugin không đè lên.
Hậu tố no text, no logo. Hình đầu bài hay bị máy vẽ thêm chữ vô nghĩa trên biển hiệu, bao bì. Gắn sẵn hậu tố này giảm hẳn chuyện đó. Chữ tiêu đề nếu cần đặt trên hình thì để giao diện trang lo.
Khổ 1536x1024. Tỉ lệ 3:2 hợp với hầu hết giao diện WordPress và cắt sang 16:9 cho Facebook vẫn đẹp. Muốn khổ màn rộng hẳn thì đổi 1664x928.
Trường ahn_hang. Mặc định Plus (500 điểm mỗi hình ảnh). Bài chân dung, thời trang thì người viết gõ pro vào trường này; cảnh nhiều người, cần bàn tay chuẩn thì max.
Thẻ alt. Lấy từ trường ahn_alt nếu có, không thì dùng tiêu đề bài. Alt tốt nhất là một câu tiếng Việt tả đúng hình ảnh, không nhồi từ khoá. Xem thêm bài lưu tệp, đặt tên, nén webp, thẻ alt.
Câu lệnh lưu kèm tệp đính kèm. Trường ahn_cau_lenh trên tệp giúp sau này biết hình ảnh vẽ từ câu nào, cần vẽ lại thì có sẵn.
Bật trường tuỳ biến trong trình soạn thảo
Trình soạn thảo khối của WordPress ẩn bảng trường tuỳ biến theo mặc định. Mở menu ba chấm ở góc phải, vào phần Tuỳ chọn, bật mục trường tuỳ biến (Custom fields), trình soạn thảo tải lại. Bảng hiện ở cuối trang soạn bài: thêm ahn_canh, ahn_alt, ahn_hang một lần, các bài sau chọn từ danh sách.
Lỗi hay gặp khi chạy thật
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Đăng xong, mãi không có hình | WP-Cron chỉ chạy khi có người vào trang; trang ít khách thì việc nằm chờ | Đặt cron thật trên máy chủ gọi wp-cron.php mỗi 5 phút |
Trường ahn_loi ghi lỗi hết giờ | Máy chủ thuê giới hạn thời gian chạy PHP 30 giây, set_time_limit không có tác dụng | Chạy WP-Cron bằng dòng lệnh (php wp-cron.php) qua cron của máy chủ |
| Hình đại diện có nhưng không có bản thu nhỏ | Thư viện GD trên máy chủ không hỗ trợ webp | Đổi output_format sang jpeg và đuôi tệp .jpg, hoặc nhờ nhà cung cấp bật webp |
401 trong ahn_loi | Thiếu hoặc sai AHN_KEY trong wp-config.php | Kiểm tra khoá, khoá có đang bật ở trang khoá API |
402 trong ahn_loi | Ví hết điểm | Nạp điểm; các bài đã lỗi phải hẹn lại bằng tay |
| Vẽ hai lần cho một bài | Hai plugin cùng xử lý hình đại diện | Tắt plugin hình đại diện tự động khác |
Dòng thứ hai đáng nói thêm. Trên nhiều gói máy chủ thuê, PHP bị giới hạn 30 giây cho mỗi yêu cầu web, kể cả yêu cầu WP-Cron gọi ngầm. Lượt vẽ hạng Plus thường xong trong khoảng 10 giây khi máy rảnh, nhưng lúc bận có thể lâu hơn nhiều. Chạy WP-Cron bằng dòng lệnh (DISABLE_WP_CRON trong wp-config.php cộng một dòng cron của máy chủ) vừa tránh giới hạn thời gian vừa không làm chậm lượt xem của khách.
Chi phí cho một trang tin
Ba bài mỗi ngày, một hình ảnh Plus mỗi bài: 3 × 500 = 1.500 điểm mỗi ngày, khoảng 45.000 điểm mỗi tháng. Nếu một phần ba số bài cần hạng Pro cho chân dung, chi phí tháng khoảng 60.000 điểm. Ví điểm chung với các dịch vụ khác của hệ AI Đỉnh; giá API bằng giá web, xem ở bảng giá.
Mở rộng
- Vẽ lại theo yêu cầu: thêm một nút nhỏ trong cột danh sách bài, gỡ hình đại diện cũ rồi gọi
ahn_ve_dau_baingay. - Chọn từ hai hình ảnh: gửi
n: 2, lưu cả hai vào thư viện, gắn hình đầu, để biên tập viên đổi nếu thích hình thứ hai. Chi phí gấp đôi. - Bài cũ chưa có hình: viết lệnh WP-CLI duyệt các bài có
ahn_canhmà chưa có hình đại diện, hẹn việc cho từng bài cách nhau một phút.
Muốn xử lý lỗi kỹ hơn, xem bài xử lý lỗi 503 và Retry-After.



