yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
visual_service_impl.cc
Go to the documentation of this file.
2
3#ifdef YAZE_WITH_GRPC
4
5#include <chrono>
6#include <filesystem>
7#include <fstream>
8
9#include "absl/strings/str_cat.h"
11#include "grpcpp/grpcpp.h"
12#include "protos/visual_service.grpc.pb.h"
13#include "protos/visual_service.pb.h"
14
15namespace yaze {
16
17namespace {
18
19// Convert proto config to C++ config
20test::VisualDiffConfig ProtoToConfig(const proto::ComparisonConfig& proto) {
21 test::VisualDiffConfig config;
22 config.tolerance = proto.tolerance() > 0 ? proto.tolerance() : 0.95f;
23 config.color_threshold =
24 proto.color_threshold() > 0 ? proto.color_threshold() : 10;
25 config.generate_diff_image = proto.generate_diff_image();
26
27 switch (proto.algorithm()) {
28 case proto::ComparisonConfig::SSIM:
30 break;
31 case proto::ComparisonConfig::PERCEPTUAL_HASH:
33 break;
34 default:
36 break;
37 }
38
39 switch (proto.diff_style()) {
40 case proto::ComparisonConfig::SIDE_BY_SIDE:
42 break;
43 case proto::ComparisonConfig::HEATMAP:
45 break;
46 default:
48 break;
49 }
50
51 // Convert ignore regions
52 for (const auto& region : proto.ignore_regions()) {
53 config.ignore_regions.push_back(test::ScreenRegion{
54 region.x(), region.y(), region.width(), region.height()});
55 }
56
57 return config;
58}
59
60// Convert C++ result to proto
61void ResultToProto(const test::VisualDiffResult& result,
62 proto::VisualDiffResult* proto) {
63 proto->set_identical(result.identical);
64 proto->set_passed(result.passed);
65 proto->set_similarity(result.similarity);
66 proto->set_difference_pct(result.difference_pct);
67 proto->set_width(result.width);
68 proto->set_height(result.height);
69 proto->set_total_pixels(result.total_pixels);
70 proto->set_differing_pixels(result.differing_pixels);
71
72 if (!result.diff_image_png.empty()) {
73 proto->set_diff_image_png(result.diff_image_png.data(),
74 result.diff_image_png.size());
75 }
76
77 proto->set_diff_description(result.diff_description);
78
79 for (const auto& region : result.significant_regions) {
80 auto* proto_region = proto->add_significant_regions();
81 proto_region->set_x(region.x);
82 proto_region->set_y(region.y);
83 proto_region->set_width(region.width);
84 proto_region->set_height(region.height);
85 proto_region->set_local_diff_pct(region.local_diff_pct);
86 }
87
88 if (!result.error_message.empty()) {
89 proto->set_error(result.error_message);
90 }
91}
92
93} // namespace
94
95// ============================================================================
96// VisualServiceImpl
97// ============================================================================
98
99VisualServiceImpl::VisualServiceImpl()
100 : reference_dir_("/tmp/yaze_visual_references") {}
101
102VisualServiceImpl::~VisualServiceImpl() = default;
103
104void VisualServiceImpl::SetReferenceImageDir(const std::string& path) {
105 reference_dir_ = path;
106 std::filesystem::create_directories(path);
107}
108
109void VisualServiceImpl::SetAIVisionVerifier(test::AIVisionVerifier* verifier) {
110 vision_verifier_ = verifier;
111}
112
113test::VisualDiffEngine& VisualServiceImpl::GetDiffEngine() {
114 if (!diff_engine_) {
115 diff_engine_ = std::make_unique<test::VisualDiffEngine>();
116 }
117 return *diff_engine_;
118}
119
120absl::Status VisualServiceImpl::ComparePngData(
121 const proto::ComparePngDataRequest* request,
122 proto::ComparePngDataResponse* response) {
123 if (request->png_a().empty() || request->png_b().empty()) {
124 return absl::InvalidArgumentError("Both PNG images must be provided");
125 }
126
127 auto& engine = GetDiffEngine();
128 if (request->has_config()) {
129 engine.SetConfig(ProtoToConfig(request->config()));
130 }
131
132 std::vector<uint8_t> png_a(request->png_a().begin(), request->png_a().end());
133 std::vector<uint8_t> png_b(request->png_b().begin(), request->png_b().end());
134
135 auto result = engine.ComparePngData(png_a, png_b);
136 if (!result.ok()) {
137 return result.status();
138 }
139
140 ResultToProto(*result, response->mutable_result());
141 return absl::OkStatus();
142}
143
144absl::Status VisualServiceImpl::ComparePngFiles(
145 const proto::ComparePngFilesRequest* request,
146 proto::ComparePngFilesResponse* response) {
147 if (request->path_a().empty() || request->path_b().empty()) {
148 return absl::InvalidArgumentError("Both file paths must be provided");
149 }
150
151 auto& engine = GetDiffEngine();
152 if (request->has_config()) {
153 engine.SetConfig(ProtoToConfig(request->config()));
154 }
155
156 auto result = engine.ComparePngFiles(request->path_a(), request->path_b());
157 if (!result.ok()) {
158 return result.status();
159 }
160
161 ResultToProto(*result, response->mutable_result());
162 return absl::OkStatus();
163}
164
165absl::Status VisualServiceImpl::CompareWithReference(
166 const proto::CompareWithReferenceRequest* request,
167 proto::CompareWithReferenceResponse* response) {
168 if (request->png_data().empty()) {
169 return absl::InvalidArgumentError("PNG data must be provided");
170 }
171 if (request->reference_path().empty()) {
172 return absl::InvalidArgumentError("Reference path must be provided");
173 }
174
175 auto& engine = GetDiffEngine();
176 if (request->has_config()) {
177 engine.SetConfig(ProtoToConfig(request->config()));
178 }
179
180 std::vector<uint8_t> png_data(request->png_data().begin(),
181 request->png_data().end());
182
183 auto result =
184 engine.CompareWithReference(png_data, request->reference_path());
185 if (!result.ok()) {
186 return result.status();
187 }
188
189 ResultToProto(*result, response->mutable_result());
190 return absl::OkStatus();
191}
192
193absl::Status VisualServiceImpl::CompareRegion(
194 const proto::CompareRegionRequest* request,
195 proto::CompareRegionResponse* response) {
196 if (request->png_a().empty() || request->png_b().empty()) {
197 return absl::InvalidArgumentError("Both PNG images must be provided");
198 }
199
200 auto& engine = GetDiffEngine();
201 if (request->has_config()) {
202 engine.SetConfig(ProtoToConfig(request->config()));
203 }
204
205 std::vector<uint8_t> png_a(request->png_a().begin(), request->png_a().end());
206 std::vector<uint8_t> png_b(request->png_b().begin(), request->png_b().end());
207
208 test::ScreenRegion region{request->region().x(), request->region().y(),
209 request->region().width(),
210 request->region().height()};
211
212 auto result = engine.CompareRegion(png_a, png_b, region);
213 if (!result.ok()) {
214 return result.status();
215 }
216
217 ResultToProto(*result, response->mutable_result());
218 return absl::OkStatus();
219}
220
221absl::Status VisualServiceImpl::GenerateDiffImage(
222 const proto::GenerateDiffImageRequest* request,
223 proto::GenerateDiffImageResponse* response) {
224 if (request->png_a().empty() || request->png_b().empty()) {
225 return absl::InvalidArgumentError("Both PNG images must be provided");
226 }
227
228 test::VisualDiffConfig config;
229 config.generate_diff_image = true;
230
231 switch (request->style()) {
232 case proto::ComparisonConfig::SIDE_BY_SIDE:
233 config.diff_style = test::VisualDiffConfig::DiffStyle::kSideBySide;
234 break;
235 case proto::ComparisonConfig::HEATMAP:
236 config.diff_style = test::VisualDiffConfig::DiffStyle::kHeatmap;
237 break;
238 default:
239 config.diff_style = test::VisualDiffConfig::DiffStyle::kRedHighlight;
240 break;
241 }
242
243 test::VisualDiffEngine engine(config);
244
245 // Decode PNGs
246 auto screenshot_a = test::VisualDiffEngine::DecodePng(
247 std::vector<uint8_t>(request->png_a().begin(), request->png_a().end()));
248 if (!screenshot_a.ok()) {
249 response->set_success(false);
250 response->set_error(std::string(screenshot_a.status().message()));
251 return absl::OkStatus();
252 }
253
254 auto screenshot_b = test::VisualDiffEngine::DecodePng(
255 std::vector<uint8_t>(request->png_b().begin(), request->png_b().end()));
256 if (!screenshot_b.ok()) {
257 response->set_success(false);
258 response->set_error(std::string(screenshot_b.status().message()));
259 return absl::OkStatus();
260 }
261
262 // Generate diff
263 auto diff_result = engine.GenerateDiffPng(*screenshot_a, *screenshot_b);
264 if (!diff_result.ok()) {
265 response->set_success(false);
266 response->set_error(std::string(diff_result.status().message()));
267 return absl::OkStatus();
268 }
269
270 response->set_success(true);
271 response->set_diff_image_png(diff_result->data(), diff_result->size());
272 return absl::OkStatus();
273}
274
275absl::Status VisualServiceImpl::RunRegressionTest(
276 const proto::RunRegressionTestRequest* request,
277 proto::RunRegressionTestResponse* response) {
278 if (request->current_screenshot().empty()) {
279 return absl::InvalidArgumentError("Current screenshot must be provided");
280 }
281 if (request->reference_id().empty()) {
282 return absl::InvalidArgumentError("Reference ID must be provided");
283 }
284
285 // Build reference path
286 std::string reference_path =
287 absl::StrCat(reference_dir_, "/", request->reference_id(), ".png");
288
289 if (!std::filesystem::exists(reference_path)) {
290 return absl::NotFoundError(
291 absl::StrCat("Reference image not found: ", request->reference_id()));
292 }
293
294 auto& engine = GetDiffEngine();
295 if (request->has_config()) {
296 engine.SetConfig(ProtoToConfig(request->config()));
297 }
298
299 std::vector<uint8_t> current(request->current_screenshot().begin(),
300 request->current_screenshot().end());
301
302 auto result = engine.CompareWithReference(current, reference_path);
303 if (!result.ok()) {
304 return result.status();
305 }
306
307 response->set_passed(result->passed);
308 ResultToProto(*result, response->mutable_result());
309 response->set_test_name(request->test_name());
310 response->set_reference_id(request->reference_id());
311 response->set_timestamp_ms(
312 std::chrono::duration_cast<std::chrono::milliseconds>(
313 std::chrono::system_clock::now().time_since_epoch())
314 .count());
315
316 return absl::OkStatus();
317}
318
319absl::Status VisualServiceImpl::ListReferenceImages(
320 const proto::ListReferenceImagesRequest* request,
321 proto::ListReferenceImagesResponse* response) {
322 if (!std::filesystem::exists(reference_dir_)) {
323 return absl::OkStatus(); // Empty list
324 }
325
326 for (const auto& entry :
327 std::filesystem::directory_iterator(reference_dir_)) {
328 if (entry.path().extension() == ".png") {
329 auto* img = response->add_images();
330 img->set_id(entry.path().stem().string());
331 img->set_path(entry.path().string());
332
333 // Apply filters
334 if (!request->category().empty()) {
335 // Category filtering would require metadata storage
336 // For now, skip filtering
337 }
338 if (!request->prefix().empty()) {
339 if (img->id().find(request->prefix()) != 0) {
340 response->mutable_images()->RemoveLast();
341 continue;
342 }
343 }
344
345 // Try to get image dimensions
346 auto screenshot = test::VisualDiffEngine::LoadPng(entry.path().string());
347 if (screenshot.ok()) {
348 img->set_width(screenshot->width);
349 img->set_height(screenshot->height);
350 }
351
352 // Get file creation time (portable: avoid file_clock::to_sys which
353 // is missing on MSVC's STL)
354 auto ftime = std::filesystem::last_write_time(entry);
355 auto file_duration = ftime.time_since_epoch();
356 auto ms =
357 std::chrono::duration_cast<std::chrono::milliseconds>(file_duration);
358 img->set_created_timestamp_ms(ms.count());
359 }
360 }
361
362 return absl::OkStatus();
363}
364
365absl::Status VisualServiceImpl::SaveReferenceImage(
366 const proto::SaveReferenceImageRequest* request,
367 proto::SaveReferenceImageResponse* response) {
368 if (request->png_data().empty()) {
369 return absl::InvalidArgumentError("PNG data must be provided");
370 }
371 if (request->reference_id().empty()) {
372 return absl::InvalidArgumentError("Reference ID must be provided");
373 }
374
375 std::filesystem::create_directories(reference_dir_);
376
377 std::string path =
378 absl::StrCat(reference_dir_, "/", request->reference_id(), ".png");
379
380 std::ofstream file(path, std::ios::binary);
381 if (!file) {
382 response->set_success(false);
383 response->set_error(
384 absl::StrCat("Failed to open file for writing: ", path));
385 return absl::OkStatus();
386 }
387
388 file.write(request->png_data().data(), request->png_data().size());
389
390 response->set_success(true);
391 response->set_path(path);
392 return absl::OkStatus();
393}
394
395absl::Status VisualServiceImpl::AnalyzeScreenshot(
396 const proto::AnalyzeScreenshotRequest* request,
397 proto::AnalyzeScreenshotResponse* response) {
398 if (!vision_verifier_) {
399 response->set_success(false);
400 response->set_error("AI vision verifier not configured");
401 return absl::OkStatus();
402 }
403
404 // TODO: Implement integration with AIVisionVerifier
405 // This would call vision_verifier_->AnalyzeImage() or similar
406
407 response->set_success(false);
408 response->set_error("AI analysis not yet implemented");
409 return absl::OkStatus();
410}
411
412absl::Status VisualServiceImpl::VerifyVisualCondition(
413 const proto::VerifyVisualConditionRequest* request,
414 proto::VerifyVisualConditionResponse* response) {
415 if (!vision_verifier_) {
416 response->set_success(false);
417 response->set_error("AI vision verifier not configured");
418 return absl::OkStatus();
419 }
420
421 // TODO: Implement integration with AIVisionVerifier
422 // This would call vision_verifier_->VerifyCondition() or similar
423
424 response->set_success(false);
425 response->set_error("Visual condition verification not yet implemented");
426 return absl::OkStatus();
427}
428
429// ============================================================================
430// gRPC Service Wrapper
431// ============================================================================
432
433class VisualServiceGrpc final : public proto::VisualService::Service {
434 public:
435 explicit VisualServiceGrpc(VisualServiceImpl* impl) : impl_(impl) {}
436
437#define IMPL_RPC(method) \
438 grpc::Status method(grpc::ServerContext* context, \
439 const proto::method##Request* request, \
440 proto::method##Response* response) override { \
441 (void)context; \
442 auto status = impl_->method(request, response); \
443 if (!status.ok()) { \
444 return grpc::Status(grpc::StatusCode::INTERNAL, \
445 std::string(status.message())); \
446 } \
447 return grpc::Status::OK; \
448 }
449
450 IMPL_RPC(ComparePngData)
451 IMPL_RPC(ComparePngFiles)
452 IMPL_RPC(CompareWithReference)
453 IMPL_RPC(CompareRegion)
454 IMPL_RPC(GenerateDiffImage)
455 IMPL_RPC(RunRegressionTest)
456 IMPL_RPC(ListReferenceImages)
457 IMPL_RPC(SaveReferenceImage)
458 IMPL_RPC(AnalyzeScreenshot)
459 IMPL_RPC(VerifyVisualCondition)
460
461#undef IMPL_RPC
462
463 private:
464 VisualServiceImpl* impl_;
465};
466
467std::unique_ptr<grpc::Service> CreateVisualServiceGrpc(
468 VisualServiceImpl* impl) {
469 return std::make_unique<VisualServiceGrpc>(impl);
470}
471
472} // namespace yaze
473
474#endif // YAZE_WITH_GRPC